How to register multiple implementations of the same interface in Asp.Net Core?

前端 未结 24 1528
不思量自难忘°
不思量自难忘° 2020-11-22 10:16

I have services that are derived from the same interface.

public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService         


        
相关标签:
24条回答
  • 2020-11-22 10:55

    I've faced the same issue and want to share how I solved it and why.

    As you mentioned there are two problems. The first:

    In Asp.Net Core how do I register these services and resolve it at runtime based on some key?

    So what options do we have? Folks suggest two:

    • Use a custom factory (like _myFactory.GetServiceByKey(key))

    • Use another DI engine (like _unityContainer.Resolve<IService>(key))

    Is the Factory pattern the only option here?

    In fact both options are factories because each IoC Container is also a factory (highly configurable and complicated though). And it seems to me that other options are also variations of the Factory pattern.

    So what option is better then? Here I agree with @Sock who suggested using custom factory, and that is why.

    First, I always try to avoid adding new dependencies when they are not really needed. So I agree with you in this point. Moreover, using two DI frameworks is worse than creating custom factory abstraction. In the second case you have to add new package dependency (like Unity) but depending on a new factory interface is less evil here. The main idea of ASP.NET Core DI, I believe, is simplicity. It maintains a minimal set of features following KISS principle. If you need some extra feature then DIY or use a corresponding Plungin that implements desired feature (Open Closed Principle).

    Secondly, often we need to inject many named dependencies for single service. In case of Unity you may have to specify names for constructor parameters (using InjectionConstructor). This registration uses reflection and some smart logic to guess arguments for the constructor. This also may lead to runtime errors if registration does not match the constructor arguments. From the other hand, when using your own factory you have full control of how to provide the constructor parameters. It's more readable and it's resolved at compile-time. KISS principle again.

    The second problem:

    How can _serviceProvider.GetService() inject appropriate connection string?

    First, I agree with you that depending on new things like IOptions (and therefore on package Microsoft.Extensions.Options.ConfigurationExtensions) is not a good idea. I've seen some discussing about IOptions where there were different opinions about its benifit. Again, I try to avoid adding new dependencies when they are not really needed. Is it really needed? I think no. Otherwise each implementation would have to depend on it without any clear need coming from that implementation (for me it looks like violation of ISP, where I agree with you too). This is also true about depending on the factory but in this case it can be avoided.

    The ASP.NET Core DI provides a very nice overload for that purpose:

    var mongoConnection = //...
    var efConnection = //...
    var otherConnection = //...
    services.AddTransient<IMyFactory>(
                 s => new MyFactoryImpl(
                     mongoConnection, efConnection, otherConnection, 
                     s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>())));
    
    0 讨论(0)
  • 2020-11-22 10:56

    since my post above, I have moved to a Generic Factory Class

    Usage

     services.AddFactory<IProcessor, string>()
             .Add<ProcessorA>("A")
             .Add<ProcessorB>("B");
    
     public MyClass(IFactory<IProcessor, string> processorFactory)
     {
           var x = "A"; //some runtime variable to select which object to create
           var processor = processorFactory.Create(x);
     }
    

    Implementation

    public class FactoryBuilder<I, P> where I : class
    {
        private readonly IServiceCollection _services;
        private readonly FactoryTypes<I, P> _factoryTypes;
        public FactoryBuilder(IServiceCollection services)
        {
            _services = services;
            _factoryTypes = new FactoryTypes<I, P>();
        }
        public FactoryBuilder<I, P> Add<T>(P p)
            where T : class, I
        {
            _factoryTypes.ServiceList.Add(p, typeof(T));
    
            _services.AddSingleton(_factoryTypes);
            _services.AddTransient<T>();
            return this;
        }
    }
    public class FactoryTypes<I, P> where I : class
    {
        public Dictionary<P, Type> ServiceList { get; set; } = new Dictionary<P, Type>();
    }
    
    public interface IFactory<I, P>
    {
        I Create(P p);
    }
    
    public class Factory<I, P> : IFactory<I, P> where I : class
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly FactoryTypes<I, P> _factoryTypes;
        public Factory(IServiceProvider serviceProvider, FactoryTypes<I, P> factoryTypes)
        {
            _serviceProvider = serviceProvider;
            _factoryTypes = factoryTypes;
        }
    
        public I Create(P p)
        {
            return (I)_serviceProvider.GetService(_factoryTypes.ServiceList[p]);
        }
    }
    

    Extension

    namespace Microsoft.Extensions.DependencyInjection
    {
        public static class DependencyExtensions
        {
            public static IServiceCollection AddFactory<I, P>(this IServiceCollection services, Action<FactoryBuilder<I, P>> builder)
                where I : class
            {
                services.AddTransient<IFactory<I, P>, Factory<I, P>>();
                var factoryBuilder = new FactoryBuilder<I, P>(services);
                builder(factoryBuilder);
                return services;
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 10:56

    I created my own extension over IServiceCollection used WithName extension:

    public static IServiceCollection AddScopedWithName<TService, TImplementation>(this IServiceCollection services, string serviceName)
            where TService : class
            where TImplementation : class, TService
        {
            Type serviceType = typeof(TService);
            Type implementationServiceType = typeof(TImplementation);
            ServiceCollectionTypeMapper.Instance.AddDefinition(serviceType.Name, serviceName, implementationServiceType.AssemblyQualifiedName);
            services.AddScoped<TImplementation>();
            return services;
        }
    

    ServiceCollectionTypeMapper is a singleton instance that maps IService > NameOfService > Implementation where an interface could have many implementations with different names, this allows to register types than we can resolve when wee need and is a different approach than resolve multiple services to select what we want.

     /// <summary>
    /// Allows to set the service register mapping.
    /// </summary>
    public class ServiceCollectionTypeMapper
    {
        private ServiceCollectionTypeMapper()
        {
            this.ServiceRegister = new Dictionary<string, Dictionary<string, string>>();
        }
    
        /// <summary>
        /// Gets the instance of mapper.
        /// </summary>
        public static ServiceCollectionTypeMapper Instance { get; } = new ServiceCollectionTypeMapper();
    
        private Dictionary<string, Dictionary<string, string>> ServiceRegister { get; set; }
    
        /// <summary>
        /// Adds new service definition.
        /// </summary>
        /// <param name="typeName">The name of the TService.</param>
        /// <param name="serviceName">The TImplementation name.</param>
        /// <param name="namespaceFullName">The TImplementation AssemblyQualifiedName.</param>
        public void AddDefinition(string typeName, string serviceName, string namespaceFullName)
        {
            if (this.ServiceRegister.TryGetValue(typeName, out Dictionary<string, string> services))
            {
                if (services.TryGetValue(serviceName, out _))
                {
                    throw new InvalidOperationException($"Exists an implementation with the same name [{serviceName}] to the type [{typeName}].");
                }
                else
                {
                    services.Add(serviceName, namespaceFullName);
                }
            }
            else
            {
                Dictionary<string, string> serviceCollection = new Dictionary<string, string>
                {
                    { serviceName, namespaceFullName },
                };
                this.ServiceRegister.Add(typeName, serviceCollection);
            }
        }
    
        /// <summary>
        /// Get AssemblyQualifiedName of implementation.
        /// </summary>
        /// <typeparam name="TService">The type of the service implementation.</typeparam>
        /// <param name="serviceName">The name of the service.</param>
        /// <returns>The AssemblyQualifiedName of the inplementation service.</returns>
        public string GetService<TService>(string serviceName)
        {
            Type serviceType = typeof(TService);
    
            if (this.ServiceRegister.TryGetValue(serviceType.Name, out Dictionary<string, string> services))
            {
                if (services.TryGetValue(serviceName, out string serviceImplementation))
                {
                    return serviceImplementation;
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }
    

    To register a new service:

    services.AddScopedWithName<IService, MyService>("Name");
    

    To resolve service we need an extension over IServiceProvider like this.

    /// <summary>
        /// Gets the implementation of service by name.
        /// </summary>
        /// <typeparam name="T">The type of service.</typeparam>
        /// <param name="serviceProvider">The service provider.</param>
        /// <param name="serviceName">The service name.</param>
        /// <returns>The implementation of service.</returns>
        public static T GetService<T>(this IServiceProvider serviceProvider, string serviceName)
        {
            string fullnameImplementation = ServiceCollectionTypeMapper.Instance.GetService<T>(serviceName);
            if (fullnameImplementation == null)
            {
                throw new InvalidOperationException($"Unable to resolve service of type [{typeof(T)}] with name [{serviceName}]");
            }
            else
            {
                return (T)serviceProvider.GetService(Type.GetType(fullnameImplementation));
            }
        }
    

    When resolve:

    serviceProvider.GetService<IWithdrawalHandler>(serviceName);
    

    Remember that serviceProvider can be injected within a constructor in our application as IServiceProvider.

    I hope this helps.

    0 讨论(0)
  • 2020-11-22 10:57

    Another option is to use the extension method GetServices from Microsoft.Extensions.DependencyInjection.

    Register your services as:

    services.AddSingleton<IService, ServiceA>();
    services.AddSingleton<IService, ServiceB>();
    services.AddSingleton<IService, ServiceC>();
    

    Then resolve with a little of Linq:

    var services = serviceProvider.GetServices<IService>();
    var serviceB = services.First(o => o.GetType() == typeof(ServiceB));
    

    or

    var serviceZ = services.First(o => o.Name.Equals("Z"));
    

    (assuming that IService has a string property called "Name")

    Make sure to have using Microsoft.Extensions.DependencyInjection;

    Update

    AspNet 2.1 source: GetServices

    0 讨论(0)
  • How about a service for services?

    If we had an INamedService interface (with .Name property), we could write an IServiceCollection extension for .GetService(string name), where the extension would take that string parameter, and do a .GetServices() on itself, and in each returned instance, find the instance whose INamedService.Name matches the given name.

    Like this:

    public interface INamedService
    {
        string Name { get; }
    }
    
    public static T GetService<T>(this IServiceProvider provider, string serviceName)
        where T : INamedService
    {
        var candidates = provider.GetServices<T>();
        return candidates.FirstOrDefault(s => s.Name == serviceName);
    }
    

    Therefore, your IMyService must implement INamedService, but you'll get the key-based resolution you want, right?

    To be fair, having to even have this INamedService interface seems ugly, but if you wanted to go further and make things more elegant, then a [NamedServiceAttribute("A")] on the implementation/class could be found by the code in this extension, and it'd work just as well. To be even more fair, Reflection is slow, so an optimization may be in order, but honestly that's something the DI engine should've been helping with. Speed and simplicity are each grand contributors to TCO.

    All in all, there's no need for an explicit factory, because "finding a named service" is such a reusable concept, and factory classes don't scale as a solution. And a Func<> seems fine, but a switch block is so bleh, and again, you'll be writing Funcs as often as you'd be writing Factories. Start simple, reusable, with less code, and if that turns out not to do it for ya, then go complex.

    0 讨论(0)
  • 2020-11-22 11:01

    It is not supported by Microsoft.Extensions.DependencyInjection.

    But you can plug-in another dependency injection mechanism, like StructureMap See it's Home page and it's GitHub Project.

    It's not hard at all:

    1. Add a dependency to StructureMap in your project.json:

      "Structuremap.Microsoft.DependencyInjection" : "1.0.1",
      
    2. Inject it into the ASP.NET pipeline inside ConfigureServices and register your classes (see docs)

      public IServiceProvider ConfigureServices(IServiceCollection services) // returns IServiceProvider !
      {
          // Add framework services.
          services.AddMvc();
          services.AddWhatever();
      
          //using StructureMap;
          var container = new Container();
          container.Configure(config =>
          {
              // Register stuff in container, using the StructureMap APIs...
              config.For<IPet>().Add(new Cat("CatA")).Named("A");
              config.For<IPet>().Add(new Cat("CatB")).Named("B");
              config.For<IPet>().Use("A"); // Optionally set a default
              config.Populate(services);
          });
      
          return container.GetInstance<IServiceProvider>();
      }
      
    3. Then, to get a named instance, you will need to request the IContainer

      public class HomeController : Controller
      {
          public HomeController(IContainer injectedContainer)
          {
              var myPet = injectedContainer.GetInstance<IPet>("B");
              string name = myPet.Name; // Returns "CatB"
      

    That's it.

    For the example to build, you need

        public interface IPet
        {
            string Name { get; set; }
        }
    
        public class Cat : IPet
        {
            public Cat(string name)
            {
                Name = name;
            }
    
            public string Name {get; set; }
        }
    
    0 讨论(0)
提交回复
热议问题