📜 ⬆️ ⬇️

Conveniently create Composition Root using Autofac

The projects that I develop and maintain are quite large in scope. For this reason, they actively use the Dependency Injection pattern.


The most important part of its implementation is the Composition Root - an assembly point, usually performed using the Register-Resolve-Release pattern. For a well-read, compact and expressive description of Composition Root, a tool such as a DI container is usually used; if there is a choice, I prefer to use Autofac .


Despite the fact that this container is deservedly considered the leader in convenience, developers have many questions and even complaints. For the most common problems from my own practice, I will describe ways that can help mitigate or completely remove almost all the difficulties associated with using Autofac as a Composition Root configuration tool.


Many configuration errors are detected only during execution.


Type safe version of As method


Minimal remedy with maximum effect:


public static IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle> AsStrict<T>( this IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle> registrationBuilder) { return registrationBuilder.As<T>(); } public static IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle> AsStrict<T>( this IRegistrationBuilder<T, ConcreteReflectionActivatorData, SingleRegistrationStyle> registrationBuilder) { return registrationBuilder.As<T>(); } 

You have to write a one-liner method for each common combination of activation data and registration style, as C # does not support partial type auto-inference for generalizations.
But the only plus pays for all costs: now the mismatch between interface types and implementation will lead to a compilation error.


Resharper replacement patterns to ease the transition to AsStrict


If you already have a fairly large project, then you can ease the implementation of the type-safe As version using custom Resharper replacement patterns .


I got one sample for each one-line. All search and replace expressions are the same:


  //  $builder$.As<$type$>() builder - expression placeholder type - type placeholder //  $builder$.AsStrict<$type>() 

But the limit on the type of $ builder $ for each sample is different:


  IRegistrationBuilder<$type$, SimpleActivatorData, SingleRegistrationStyle> IRegistrationBuilder<$type$, ConcreteReflection, SingleRegistrationStyle> 

Use Register instead of RegisterType


This can also be useful:


  1. You can fine-tune the build implementation
  2. Compared to registering a type, it is simpler and clearer to explicitly pass parameters
  3. Higher performance when resolving implementations

… but there are also disadvantages:


  1. Any change to the class constructor signature will require correction of the registration code.
  2. Type registration is noticeably smaller than the simplest delegate registration.

It is difficult to understand exactly which type is registered through the delegate.


It is better to always specify the interface type using AsStrict, except when using RegisterType <> () with identical interface types and implementations. The bonus will fail when compiling if the interface types and the values ​​returned by the delegate are incompatible


Registration of implementations via delegate takes up too much space


Sometimes more than one line can be too much, especially if it is because of it that the set of registrations stops being placed on the screen.


The easiest way to allocate registration is through the delegate to the extension method for ContainerBuilder.


 public static IRegistrationBuilder<Implementation, SimpleActivatorData, SingleRegistrationStyle> RegisterImplementation( this ContainerBuilder builder) { return builder.Register(c => { //     // ... return implementation; }); } 

Use better in combination with the previous method.


 builder.RegisterImplementation().AsStrict<IInterface>(); 

Difficult to find registered name delegate registration


Autofac is able to resolve the values ​​of such delegates through auto-linking , but there are some nuances:


  1. If the parameters of the anonymous delegate (Func) of the constructor are matched by type, then the parameters of the named delegates are named.
  2. If the type of the value returned by an anonymous delegate is immediately visible, then for the named one, you must first go to its definition

As a result, named delegates create two additional levels of indirection at once - one when searching for the corresponding registration, the second when comparing the constructor parameters.


Failure to use named delegates


If there are no parameters of the same type in the constructor, then replacing it with an anonymous delegate is elementary.
Otherwise, you can replace a set of parameters with a structure, class, or interface (a la EventArgs), which must be registered separately.


Explicit registration of named delegates


This option is more correct in terms of the independence of business entities from the DI container, successfully eliminates additional indirection, but requires more verbose registration.


Difficult to maintain the necessary order of initialization of components


It would seem that this problem should not exist in a project built on the DI pattern. But it may always be necessary to use external frameworks, libraries, separate classes that are designed differently. Also, the contribution is often made by inherited code.
Traditionally, for the Dependency Injection pattern, the sequence is replaced by dependency.


RegisterInstance Remedy


Any call to RegisterInstance is a de facto Resolve, which should not be the case during registration. Even a predefined implementation is better to register as a SingleInstance.


Creating custom initialization classes


For any initializing action that is considered atomic within your Composition Root, a separate class is created. The action itself is performed in the constructor of this class.
If finalization is needed, the usual IDisposable is implemented.
Each such class is inherited from the IInitializer marker interface.
Composition Root uses Resolve


 context.Resolve<IEnumerable<IInitialization>>(); 

Ordering initialization


If some initialization actions are required to be performed later than others, then in a later action it is sufficient to use the reference to the interface implemented by the earlier one. If there is no such reference (there is only a requirement for a specific procedure), then the initialization class of the earlier action is marked with a marker interface, and a parameter of the corresponding type is added to the designer of the "late" initializer.
The result will be the following buns:


  1. The complex initialization procedure is broken down into small, simple, easily implemented, reusable parts.
  2. Autofac itself builds the correct order of initialization when adding, deleting or modifying initializers
  3. Autofac automatically detects the presence of cycles and gaps in the requirements of this order.
  4. Actually, the implementation of the RRR pattern is easily imposed in a separate class that does not depend on a specific module or project.

The only drawback is the loss of visualization of initialization as a whole, which I personally do not consider a problem, since this is easily filled by well-thought logging.


Composition Root is too big


The Autofac documentation recommends using your own Module class heirs. It helps a little, or rather, helps a little. The thing is that the modules themselves are not separated from each other. Nothing prevents a class registered in one module from being dependent on a class in another. And re-registration of the implementation of the same interface in another module is not excluded.


Composition Root Decomposition


Autofac allows you to split one monolithic Composition Root into a set of root and children using the very limited possibility of component registration described in the documentation when creating LifetimeScope .


With child assembly points, you can perform the exact same operation and repeat until the description of registrations for each specific point of the assembly enters a reasonable frame from your point of view (for me this is one screen).


Eliminate InstanceForLifetimeScope


By starting to use component registration when creating LifetimeScope, you can immediately get another tasty bun: a complete rejection of InstanceForLifetimeScope and InstancePerMatchedLifetimeScope . It is enough just to register these components as a SingleInstance in their native LifetimeScope. Along the way, the dependence on the LifetimeScope tags disappears and it becomes possible to use them as you see fit; in my case, each LifetimeScope receives a unique, human-readable name as a tag.


Convenient registration of children Composition Root


Unfortunately, direct use of the BeginLifetimeScope method is nontrivial. But this grief can be helped using the following method:


 /// <summary> ///       /// </summary> /// <typeparam name="T"> </typeparam> /// <typeparam name="TParameter">    ( )</typeparam> /// <param name="builder"> </param> /// <param name="innerScopeTagResolver">    </param> /// <param name="innerScopeBuilder">       -    </param> /// <param name="factory">         </param> /// <returns></returns> public static IRegistrationBuilder<Func<TParameter, T>, SimpleActivatorData, SingleRegistrationStyle> RegisterWithInheritedScope<T, TParameter>( this ContainerBuilder builder, Func<IComponentContext, TParameter, object> innerScopeTagResolver, Action<ContainerBuilder, IComponentContext, TParameter> innerScopeBuilder, Func<IComponentContext, IComponentContext, TParameter, T> factory) { return builder.Register<Func<TParameter, T>>(c => p => { var innerScope = c.Resolve<ILifetimeScope>().BeginLifetimeScope(innerScopeTagResolver(c, p), b => innerScopeBuilder(b, c, p)); return factory(c, innerScope, p); }); } 

This is the most common use case that allows you to create a factory with parameter passing and tag generation for child scopes (a separate child scop is created for each object that implements the T interface).


An important point: you should take care of the timely cleaning of the internal scopa. This is where an idea from one of my previous articles can help.


Pros:


  1. External skoup does not depend on the internal.
  2. Everything that is registered in the external scopa is available in the internal one.
  3. Registrations in the internal skoupe can easily block the external.

Minuses:


  1. The internal skoup gets all the charm of inheriting realizations into the load .

The following method allows you to fully control the dependence of the internal scopa on the external (including the option with complete isolation).


 public static IRegistrationBuilder<Func<TParameter, T>, SimpleActivatorData, SingleRegistrationStyle> RegisterWithIsolatedScope<T, TParameter>( this ContainerBuilder builder, Func<IComponentContext, TParameter, object> innerScopeTagResolver, Action<ContainerBuilder, IComponentContext, TParameter> innerScopeBuilder, Func<IComponentContext, IComponentContext, TParameter, T> factory) { return builder.Register<Func<TParameter, T>>(c => p => { var innerScope = new ContainerBuilder().Build().BeginLifetimeScope( innerScopeTagResolver(c, p), b => innerScopeBuilder(b, c, p)); return factory(c, innerScope, p); }); } 

Results


  1. For the full application of dependency injection in difficult cases, both a suitable tool (container) and advanced developer skills in its use are required.
  2. Even such a flexible, powerful and well-documented container like Autofac requires a certain refinement of the file for the needs of a specific project and a specific team.
  3. Decomposition of assembly points using Autofac is quite possible, the implementation of such an idea is relatively simple, although not described in the official documentation.
  4. Autofac modules are not suitable for decomposition because they do not provide encapsulation.

PS: Additions and criticism are traditionally welcome.


')

Source: https://habr.com/ru/post/269479/


All Articles