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.
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.
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>
This can also be useful:
… but there are also disadvantages:
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
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>();
Autofac is able to resolve the values of such delegates through auto-linking , but there are some nuances:
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.
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.
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.
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.
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.
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>>();
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:
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.
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.
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).
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.
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:
Minuses:
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); }); }
PS: Additions and criticism are traditionally welcome.
Source: https://habr.com/ru/post/269479/
All Articles