Skip to content

Dependency injection

The ITsynch Suite provides out-of-the-box all the necessary features and objects to register dependencies in the DI container using conventions. This features integrates seamlessly with IServiceCollection.

The aim of this feature is to isolate developers from the burden of registering dependencies, and let the Suite Framework do it for you. As can be seen below, convenient API is available to deal with component registration activities easily.

How does the dependency engine work

The engine will look for dependencies tagged with special marks telling the engine to consider them as dependencies to be registered. Any type can be given a set of properties to fine tune how registration must be carried on, for example in terms of lifetime, or under which type (let's call it register-as-type from now on) the dependency should be registered.

For those tagged dependencies located in the module's assembly, the Dependency Engine will discover and register them. If you need to register dependencies located in assemblies different from the module's, you'll have to tell the Dependency Engine how to found them.

The Suite Framework provides a set of extensions to perform the registration an easy and convenient way. Bear in mind following considerations to address the dependency registration process:

  • Use tags whenever is possible, either attribute or interface ones, in your application modules to tag dependencies conventionally.
  • Use, as much as possible, the tools described here to register dependencies. This way the framework will be able to handle those dependencies adding or removing behavior based on features and modules selected.

Dependency tags

The Suite Framework provides two options for tagging dependencies wanted to be registered by the framework:

  • using attribute tags
  • using interface tags

Attribute tags

Attribute tags allows us to mark a dependency using aspects. The available attribute tags, and their meaning for the framework, are:

  • TransientDependencyAttribute: registers the type used in with Transient lifetime in DI container
  • SingletonDependencyAttribute: registers the type used in with Singleton lifetime in DI container
  • ScopedDependencyAttribute: registers the type used in with Scoped lifetime in DI container

Let's see an example:

C#
1
2
3
4
5
6
7
8
[SingletonDependency]
public class Foo
{
    public void Bar()
    {
        // ...
    }
}

Foo class will be picked up by the framework during bootstrapping and registered as dependency in the DI container.

Note

By default all dependencies will be registered with register-as-type as the same type being registered. This means that every single dependency can be resolved or injected by DI container by using the it's own type. No matter if you specified additional types as register-as-type, the framework always allows you to resolve dependencies by using its own type. Regarding the previous example, if we want to get, or inject, Foo type from DI container, all you need to do is:

C#
    var fooInstance = serviceProvider.GetService<Foo>();

Register-as-type feature

Dependency attributes also allows us to tell the framework to register the dependency to be resolved by types other than the dependency type itself. In such cases dependency attributes can be used to indicate which types should be used as register-as-type for the dependency. Let's see an example.

C#
1
2
3
4
5
6
7
8
[SingletonDependency(typeof(IMyInterface))]
public class Foo
{
    public void Bar()
    {
        // ...
    }
}

Note

When using register-as-type please ensure that the type is assignable, or implements, that type used as registration. There is no way at compile time to give a hint on this condition, we relay on the developer responsibility to check it out when specifying register-as-type types for a dependency.

All these statements , behaviors, and comments, described here are valid and rule for all attributes, including TransientDependencyAttribute and ScopedDependencyAttribute.

Interface tags

Attribute tags allows us to mark a dependency using an interface marker. The available interface tags, and their meaning for the framework, are:

-ITransientDependency: registers the type used in with Transient lifetime in DI container -ISingletonDependency: registers the type used in with Singleton lifetime in DI container -IScopedDependency: registers the type used in with Scoped lifetime in DI container

The role of inheritance on tagged ancestors

Descendants of ancestors tagged as dependency will be discovered as dependencies as well. This characteristic become useful when you want the framework to discover a family of types, for example IMyFeature implementations will be discovered if IMyFeature is tagged with attribute dependency marker or by extending any concrete IDependency interface maker.

Difference between interface and attribute tags

Despite of the fact that both registration ways provides more or less the same functionality, attribute markers allows us to specify register-as-type for the dependency, this is the only feature, by the moment, that makes some difference.

Discovering Types from all modules

Besides the conventional registration made by the framework automatically, based on tagged types where the developer needs to do nothing to have the registration process happening, for those types located in module's assembly, there are two main needs to register types manually on your own:

  • When you need to register types from known Suite Modules which are not tagged with dependency tags, and therefore cannot be registered conventionally.
  • When you need to register types coming from third party libraries.

To register types from known Suite Modules yo can use available extension methods on ModuleConfigurationContext instance in module's ConfigureServices method, as shown here below:

C#
1
2
3
4
5
6
    public override void ConfigureServices(
        IServiceCollection services,
        ModuleConfigurationContext context)
    {
        context.RegisterTypesFromAllModules<ISomeDependency>(services);
    }

The RegisterTypesFromAllModules will search for the dependencies in all known Suite Modules, then it will register the types as dependencies in DI. Please note that RegisterTypesFromAllModules method accepts optional parameters allowing you to specify more options for the registration process. It is possible also perform a search on the module types using FindTypesFromAllModules extension method on ModuleConfigurationContext and then register those types, as we can see as follows:

C#
1
2
3
4
5
6
7
8
    public override void ConfigureServices(
        IServiceCollection services,
        ModuleConfigurationContext context)
    {
        context
            .FindTypesFromAllModules<ISomeDependency>()
            .Register(services, LifetimeType.Singleton, typeof(ISomeDependency));
    }

In the other hand, if you need to register types located in third party assembly you have to take into account if those types are tagged and can be recognized as dependencies, thus performing a conventional registration, or they are types without any dependency tag.

To conventionally register types located in assemblies other than module's, you can use the following methods:

C#
1
2
3
4
    // get the assembly reference
    var assembly = typeof(MyType).Assembly;
    // register assembly types conventionally
    assembly.Register(services);

You don't need to provide further information for the registration process because all the registration is addressed by convention, using the dependency tags contained in each Type itself.

To register types in a completely manually manner, you can use the Register extension methods on IEnumerable<Type> available in DependencyInjection namespace. In the next example the types contained in a given assembly are searched and then registered.

C#
1
2
3
4
5
    // search for implementors of ISomeInterface
    var types = this.GetType().Assembly
        .GetTypes()
        .Where(t => typeof(ISomeInterface).IsAssignableFrom(t));
    types.Register();

The framework provide some utilities to find types in assemblies or collections, which are located in the ITsynch.Suite.Extensions.Utils namespace. These methods simplify the search process providing a more expressive API, such as:

C#
1
2
3
    // search for implementors of ISomeInterface
    var types = this.GetType().Assembly.Assembly.FindSubTypesOrImplementorsOf<ISomeInterface>();
    types.Register();

Important

If you need to deal with low level methods to register dependencies, other than those provided by the high level API, please let the Suite's dev team to know about it before do that, give us the chance to validate the scenario and eventually to improve the high level API surface covering new scenarios to isolate the developer from the burden of low level registration tasks.

Some common scenarios

Lets see in examples what can be done to register types using the features provided by the Suite Framework.

Register dependencies defined in module's assembly

As we've seen before, all tagged dependencies located in the same assembly will be found and registered automatically, based on conventions.

Given the following type definitions:

C#
1
2
3
4
[SingletonDependency]
public class Foo()
{
}

and

C#
1
2
3
public class Foo() : ISingletonDependency
{
}

when Foo is located in the module's assembly, produces the same result:

  • Foo dependency will be discovered and registered in the DI container
  • The registration will be done using Singleton lifetime type.

Register dependencies with its matching interface

When a dependency implements an interface that match exactly, thus class is named as its interface without the leading "I" letter, the dependency will be registered using the type itself and its matching interface as well. Let's see some examples to illustrate:

Given an interface called IFoo, if class Foo implements it and it is tagged as dependency:

C#
1
2
3
4
5
[SingletonDependency]
public class Foo() : IFoo
{

}

produces this result:

  • Foo dependency will be discovered and registered in the DI container
  • The registration will be done using Singleton lifetime type.
  • Foo will be registered with register-as-type Foo and IFoo as well

This means that, later on, you will be able to get an instance of Foo out of the DI container, either by using its type or its implementing interface, as you can see as follows:

C#
1
2
3
4
5
// get Foo instance by its type
var foo = services.Get<Foo>();

// get Foo instance by its matching interface type
var foo = services.Get<IFoo>();

Register dependencies to be resolved as other type / or types (register-as-type)

In some circumstances there won't be possible to match interface implementations, but we want to register types to be resolved out of DI container apart from the dependency type itself. In those cases we need to tell de dependency injection engine to register that type using some specific register-as-type value (or values). Let's see some example as follows.

Given a Foo class that, wanting it to be resolved under IMyInterface interface afterwards, all you have to do is:

C#
1
2
3
4
[SingletonDependency(typeof(IMyInterface))]
public class Foo() : IMyInterface
{
}

produces this result:

  • Foo dependency will be discovered and registered in the DI container
  • The registration will be done using Singleton lifetime type.
  • Foo will be registered with register-as-type Foo and IMyInterface as well

This means that, later on, you will be able to get an instance of Foo out of the DI container, either by using its type or its implementing interface, as you can see as follows:

C#
1
2
3
4
5
// get Foo instance by its type
var foo = services.Get<Foo>();

// get Foo instance by its matching interface type
var foo = services.Get<IMyInterface>();

Default Dependency lifetime

By default, when no information about lifetime is supplied, the lifetime type of the dependency will be set to Transient.

Advanced scenarios

As we stated before, reaching this point is a sign for the Suite's dev team of some missed out methods in the high level API. If you found here, please contact the Suite dev team before go further.

In next paragraphs you will see the available methods to deal with low level registration tasks.

How registration works under the hoods

The registration is done by mean of IRegistration implementations provided by each module during bootstrapping. Those IRegistration instances will be executed by the framework after all modules are loaded, this is after all modules executed SetupModule method, following dependency graph based on dependency. At that time all registrations are performed on the IServiceCollection instance provided by .NET, the registration outcome of the overall IRegistration instances is gathered and is accessible later on in ConfigureModuleContext parameter of the ConfigureServices module's method.

ConventionalRegistration is the base class for classes in charge of registration process. TaggedDependenciesConvention is the implementation used by the framework to register tagged dependencies. You will never have to deal with this class because the framework does for you. This convention have pre-configured definition of how to discover and register dependencies.

There is also another important implementation of ConventionalRegistration named CustomConvention, which you can use to build your own strategy on how to register dependencies upon. First thing to notice is that this class is internal (intentionally), you will interact with it through RegistrationBuilder, which in turns you can handle use through specific extensions method allowing you to configure low level registration details.

Besides the available abstractions mentioned before, the recommended way to crate new registrations is by using TypeRegistration static class. This class provides a set of utility methods allowing you to configure the registration process with fine-grained configuration settings.