Skip to content
This repository has been archived by the owner on Feb 14, 2022. It is now read-only.

Tutorial

Jacopo edited this page Dec 5, 2020 · 10 revisions

Tutorial

Table of Contents

Introduction

Dependency Injection

StackInjector is a dependency injection framework.

from Wikipedia:

dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical "using" relationship the receiving object is called a client and the passed (that is, "injected") object is called a service. The code that passes the service to the client can be many kinds of things and is called the injector.

Instead of the client specifying which service it will use, the injector tells the client what service to use. The "injection" refers to the passing of a dependency (a service) into the object (a client) that would use it.

Practically this means you as a developer don't worry about actually wiring and instantiating classes, you just have to design and implement them. The Injector will do the dirty work for you.

Another big point is that in this pattern, classes are services for other "user" classes.

Practically

In this framework there are 2 main important attributes: [Service] and [Served]. Those can be found in the StackWrapper.Attributes namespace.

[Service]

Marks a class to be used as a service, and indicates important properties about the injection process.

It is used as in the following example:

[Service]
class MyClass
{
    // implementation here
}

You can also specify the service version, injection methods, etc. Those will be covered further in the tutorial.

[Served]

Indicates a property or field should be injected. Works with private properties too.

It is used as in the following example:

[Service]
class MyClass
{
    [Served]
    AnotherService anotherService;

    [Served]
    FooService fooService {get; set;}

    [Served]
    IServiceInterface gooService {get; set;}

    // other parts of the implementation here
}

You can also specify a requested version and the versioning method. Those will be covered further in the tutorial.

The opposite attribute is the [Ignored] attributes, which marks a field or property to be ignored from injection. This is to suit a different coding style, and will be covered later in the tutorial.

Wrappers

This library in particular manages the injection process by wrapping the instantiated class structure into a StackWrapper. There are two wrappers, a synchronous and an asynchronous one which will be covered later in the tutorial.

The function of those classes (publicly exposed only as interfaces) is to manage resources and track the instantiated classes.

They also expose type-safe methods to initialize and work with you dependency-injected class structure.

This also means you can have a contained environment to run your class structure into, independently from other structures; or not, if you decide to share the instances: you have complete control over how those wrappers work.

Wrappers also implement IDisposable, meaning you can use the using keyword to automatically dispose of all instances and pending processes inside the wrapper.

The Injector class

To inject your classes you must use the Injector static class, which exposes methods to perform the injection process and wrap the structure into an handy wrapper, StackWrapper, which is returned.

See StackWrappers

Injection is performed starting from a base class, the entry point of the injection and of your logic.

⚠️ your entry point can be an interface of which an implementation will be searched, or a class marked as [Service].

Synchronous

The most simple StackWrapper, simply wraps your class structure and exposes the entry point.

The following example shows how to inject starting from an example Application class:

var wrapper = Injector.From<Application>();

You can then initialize your logic mainly in two ways (you can choose the style that most suits you):

using Start, which supports an overload returning the results of the delegate

wrapper.Start( app => app.MyLogic() );

or by directly calling the method on the entry point.

wrapper.Entry.MyLogic();

Asynchronous

Asynchronous StackWrappers do offer a different approach: instead of calling a method, you submit an item to be elaborated by the stack asynchronously and wait for a result (still asynchronously).

This means that a CancellationToken must be managed in custom logic.

The following examples show how to inject starting from an example Application class (you can choose the style that most suits you):

The example class with all possible methods:

[Service]
class Application
{
    // injected services here

    public async Task<string> MyLogicAsync ( int item, CancellationToken token )
    {
        if (token.IsCancellationRequested)
            return "";

        // some logic with await
    }

    public string MyLogic ( int item )
    {
        // some simple logic here
    }
}

specify the types in the method

Injector.AsyncFrom<Application,int,string>( (app,item,token) => app.MyLogicAsync(item,token) );

let the types be derived from the delegate

Injector.AsyncFrom
    ( 
        (Application app, int item, CancellationToken token) 
            => app.MyLogicAsync(item,token) 
    );
Injector.AsyncFrom<Application,int,string>
    (
        async (app,i,t) => await Task.Run( ()=>app.MyLogic(i), t )
    );

Remember that you can use any type you want, even a tuple to pass multiple parameters at once.

StackWrappers

As the name suggest, StackWrappers do wrap your injected classes, exposing useful methods to manage your dependency injected instances.

Access from within

You might need to access the wrapper of a service inside its code. This can be achieved easily by first having the setting enabled (RegisterWrapperAsService(true)) and requesting a valid interface for your wrapper.

There are multiple interfaces in the wrapper hierarchy, and depending on what you want to do, you should declare a served field with the most restricted one.

  • ICloneableCore: cloning the core of the containing wrapper.
  • IStackWrapperCore: access settings, search for services.
  • IAsyncStackWrapperCore<T>: if you know you're in an async StackWrapper and you know T.
  • IStackWrapper<T> and IAsyncStackWrapper<TEntry, TIn, TOut>: if you know the type of wrapper you're inside and want to access every method.

Remember that if you request the wrong type of StackWrapper to be injected the implementation won't be found and an exception will be thrown.

Cloning

If you need multiple instantiations you might consider cloning and sharing the same instances across different wrappers.

In those cases cloning comes handy, since it allows to "clone" a wrapper and use its core and share instances.

To clone a wrapper, you can call

  • CloneCore: clone the wrapper and completely share core and instances
  • DeepCloneCore: clone only the registered types, the new object will be completely independent from the original one.

which will convert the wrapper to an intermediary object, which can then be converted to the wrapper you prefer; similarly on how you would initialize new StackWrapper using the Injector static class.

For example:

[Service]
class MyClass
{
  [Served]
  readonly ICloneableCore wrapper;

  private IStackWrapper<MyOtherEntryClass> innerWrapper;
  

  public void MyLogic ()
  {
    // some logic here
    this.innerWrapper = this.wrapper.CloneCore().ToWrapper<MyOtherEntryClass>();

    innerWrapper.Entry.DoSomething(someVariable);

  }
}

it's also possible to use ToAsyncWrapper<TEntry, TIn, TOut> to convert to an AsyncStackWrapper.

Get Services

Once injected, you can access the instances in your structure like in the following example:

var wrapper = Injector.From<IMyBaseClass>();

var foos_generic = wrapper.GetServices<IMyFooService>();
var coolFoo = wrapper.GetServices<CoolFooService>().First();

There isn't GetService at singular, since the approach is to consider multiple possible implementations/extensions for every possible service.

If you have only one implementation, you can use LINQ to request the first (and only) element of the list.

If no implementation is found, an empty list is returned.

Settings

To best customize the injection process, the StackWrapperSettings class contains a series of utilities and options to customize the whole injection process.

General usage

You cannot directly instantiate a new StackWrapperSettings, you must modify one of the given ones (see default options under settings) with a builder-like pattern, as in the following example:

var settings = 
      StackWrapperSettings.Default
      .TrackInstantiationDiff()
      .VersioningMethod(ServedVersionTargetingMethod.LatestMajor)
      .WhenNoMoreTasks(AsyncWaitingMethod.Timeout, 10000);

Assembly registration

Registering an assembly is much like putting it's types into a whitelist for injection.

Normally you don't have to worry about this if you use only one project and don't unset the RegisterEntryAssembly() option.

You can register an assembly in multiple ways:

// registerAssemblies has params Assembly[] as input,
// so you can simply list all of them in one place.
StackWrapperSettings.Default
  .RegisterAssemblies( 
    Assembly.LoadFrom("Cool.dll"),
    Assembly.LoadFrom("SuperCool.dll") 
  );

or if you just need know a class inside of that assembly:

StackWrapperSettings.Default
  .RegisterAssemblyOf<MyClassInAnotherAssembly>()
  .RegisterAssemblyOf<MyOtherClassInAnotherAssembly>();

or you can lazily register every non-system assembly. note: RegisterDomain takes an optional regex to filter out system assemblies

// ignores "^(System)|(Microsoft)"
StackWrapperSettings.Default
  .RegisterDomain();

Global overrides

Some settings offer the option to override the preferences in the attributes of your classes.

Normally, if no preference is set in an attribute (for example the versioning method) the one specified in the settings will be used. If this attribute is set then this attribute will override the settings; but if you explicitly set the @override parameter to true, the preference in the StackWrapperSettings specified for the injection will be used anyway.

Features

This framework has some cool features that are gonna help you develop your application.

You can find detailed explanations and a list of every feature available in the features page