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 withTransient
lifetime in DI containerSingletonDependencyAttribute
: registers the type used in withSingleton
lifetime in DI containerScopedDependencyAttribute
: registers the type used in withScoped
lifetime in DI container
Let's see an example:
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# | |
---|---|
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# | |
---|---|
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# | |
---|---|
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# | |
---|---|
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# | |
---|---|
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# | |
---|---|
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# | |
---|---|
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:
and
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:
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# | |
---|---|
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:
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# | |
---|---|
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.