📜 ⬆️ ⬇️

Conditional dependency injection in ASP.NET Core. Part 2



In the first part of the article, we showed the dependency injection settings for implementing conditional dependency injection using the Environment and Configuration mechanisms, as well as getting the service as part of an HTTP request based on the request data.

In the second part, you will see how you can expand the capabilities of the dependency injector, using the example of selecting the necessary implementation by identifier during the execution of the application.

Receiving service by identifier (Resolving service by ID)


Many popular IoC frameworks provide roughly the following functionality that allows you to assign names to specific types that implement interfaces:
')
var container = new UnityContainer(); //     Unity... container.RegisterType<IService, LocalService>("local"); container.RegisterType<IService, CloudService>("cloud"); IService service; if (context.IsLocal) { service = container.Resolve<IService>("local"); } else { service = container.Resolve<IService>("cloud"); } 

or so:

 public class LocalController { public LocalController([Dependency("local")] IService service) { this.service = service; } } public class CloudController { public CloudController([Dependency("cloud")] IService service) { this.service = service; } } 

This allows us to choose the implementation we need, depending on the context.

The dependency injector built into ASP.NET Core supports multiple implementations, but unfortunately it does not have the ability to assign identifiers for a separate implementation. Fortunately :) you can implement the service resolution yourself by identifier by writing some code.

One way to implement this functionality was to extend the ServiceDescriptor class with the ServiceName property and use it to get a service. But after studying the source codes, it became clear that access to the ServiceProvider implementation is closed (the access modifier class is internal), and we will not be able to change the logic of the GetService method.

Having abandoned the idea of ​​using reflection, as well as writing our own ServiceProvider , we decided to store the structure of matching the name and type of service directly in the container to use it when getting the service. How to influence the logic of the service, described in the first part of the article.

To begin with, we will create a structure that will contain a mapping of identifiers and corresponding implementations for each interface.

This will be a dictionary of this kind:

 Dictionary<Type, Dictionary<string, Type>> 

Here, the key of the dictionary is the type of interface, and the value is the dictionary in which (I apologize for the tautology) the key is the identifier, and the value is the type of interface implementation.

We will add services to this structure as follows:

 private readonly Dictionary<Type, Dictionary<string, Type>> serviceNameMap = new Dictionary<Type, Dictionary<string, Type>>(); public void RegisterType(Type service, Type implementation, string name) { if (this.serviceNameMap.ContainsKey(service)) { var serviceNames = ServiceNameMap[service]; if (serviceNames.ContainsKey(name)) { /* overwrite existing name implementation */ serviceNames[name] = implementation; } else { serviceNames.Add(name, implementation); } } else { this.serviceNameMap.Add(service, new Dictionary<string, Type> { [name] = implementation }); } } 

And this is how we will get the service from the container (as you remember from the previous article , the IoC container in ASP.NET Core is represented by the IServiceProvider interface):

 public object Resolve(IServiceProvider serviceProvider, Type serviceType, string name) { var service = serviceType; if (service.GetTypeInfo().IsGenericType) { return this.ResolveGeneric(serviceProvider, serviceType, name); } var serviceExists = this.serviceNameMap.ContainsKey(service); var nameExists = serviceExists && this.serviceNameMap[service].ContainsKey(name); /* Return `null` if there is no mapping for either service type or requested name */ if (!(serviceExists && nameExists)) { return null; } return serviceProvider.GetService(this.serviceNameMap[service][name]); } 

It now remains to write a set of extension methods for conveniently configuring the container, for example:

 public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services, string name) where TService : class where TImplementation : class, TService { return services.Add(typeof(TService), typeof(TImplementation), ServiceLifetime.Scoped, name); } 

 private static IServiceCollection Add(this IServiceCollection services, Type serviceType, Type implementationType, ServiceLifetime lifetime, string name) { var namedServiceProvider = services.GetOrCreateNamedServiceProvider(); namedServiceProvider.RegisterType(serviceType, implementationType, name); services.TryAddSingleton(namedServiceProvider); services.Add(new ServiceDescriptor(implementationType, implementationType, lifetime)); return services; } private static NamedServiceProvider GetOrCreateNamedServiceProvider(this IServiceCollection services) { return services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(NamedServiceProvider))?.ImplementationInstance as NamedServiceProvider ?? new NamedServiceProvider(); } 

In the code above, we add an identifier to the type and name matching structure, and the type of implementation is simply put in a container. Method of obtaining service by identifier:

 public static TService GetService<TService>(this IServiceProvider serviceProvider, string name) where TService : class { return serviceProvider .GetService<NamedServiceProvider>() .Resolve<TService>(serviceProvider, name); } 

Everything is ready for use:

 services.AddScoped<IService, LocalhostService>("local"); services.AddScoped<IService, CloudService>("cloud"); var service1 = this.serviceProvider.GetService<IService>("local"); // resolves LocalhostService var service2 = this.serviceProvider.GetService<IService>("cloud"); // resolves CloudService 

You can go a little further and create an attribute that allows you to inject an action parameter, like the MVC Core attribute [FromServices] , with this syntax:

 public IActionResult Local([FromNamedServices("local")] IService service) { ... } 

In order to implement this approach, you need a little deeper understanding of the process of binding the model (Model binding) in ASP.NET Core .

In short, the parameter attribute determines which ModelBinder (the class that implements the IModelBinder interface) will create the parameter object. For example, the [FromServices] attribute included in ASP.NET Core MVC indicates that the IoC container will be used to bind the model, therefore the ServicesModelBinder class will be used for this parameter, which will try to get the parameter type from the IoC container.

In our case, we will create two additional classes. The first is the ModelBinder , which will receive the service from the IoC container by identifier, and the second is its own FromNamedServices attribute, which will accept the service identifier in the constructor, and which will indicate that you should use the specific ModelBinder to bind .

 [AttributeUsage(AttributeTargets.Parameter)] public class FromNamedServicesAttribute : ModelBinderAttribute { public FromNamedServicesAttribute(string serviceName) { this.ServiceName = serviceName; this.BinderType = typeof(NamedServicesModelBinder); } public string ServiceName { get; set; } public override BindingSource BindingSource => BindingSource.Services; } 

 public class NamedServicesModelBinder : IModelBinder { private readonly IServiceProvider serviceProvider; public NamedServicesModelBinder(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var serviceName = GetServiceName(bindingContext); if (serviceName == null) return Task.FromResult(ModelBindingResult.Failed()); var model = this.serviceProvider.GetService(bindingContext.ModelType, serviceName); bindingContext.Model = model; bindingContext.ValidationState[model] = new ValidationStateEntry { SuppressValidation = true }; bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } private static string GetServiceName(ModelBindingContext bindingContext) { var parameter = (ControllerParameterDescriptor)bindingContext .ActionContext .ActionDescriptor .Parameters .FirstOrDefault(p => p.Name == bindingContext.FieldName); var fromServicesAttribute = parameter ?.ParameterInfo .GetCustomAttributes(typeof(FromServicesAttribute), false) .FirstOrDefault() as FromServicesAttribute; return fromServicesAttribute?.ServiceName; } } 

That's all :) The source code for the examples can be downloaded here:

github.com/nix-user/AspNetCoreDI

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


All Articles