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

前端 未结 24 1526
不思量自难忘°
不思量自难忘° 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 11:02

    Apparently, you can just inject IEnumerable of your service interface! And then find the instance that you want using LINQ.

    My example is for the AWS SNS service but you can do the same for any injected service really.

    Startup

    foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
    {
        services.AddAWSService<IAmazonSimpleNotificationService>(
            string.IsNullOrEmpty(snsRegion) ? null :
            new AWSOptions()
            {
                Region = RegionEndpoint.GetBySystemName(snsRegion)
            }
        );
    }
    
    services.AddSingleton<ISNSFactory, SNSFactory>();
    
    services.Configure<SNSConfig>(Configuration);
    

    SNSConfig

    public class SNSConfig
    {
        public string SNSDefaultRegion { get; set; }
        public string SNSSMSRegion { get; set; }
    }
    

    appsettings.json

      "SNSRegions": "ap-south-1,us-west-2",
      "SNSDefaultRegion": "ap-south-1",
      "SNSSMSRegion": "us-west-2",
    

    SNS Factory

    public class SNSFactory : ISNSFactory
    {
        private readonly SNSConfig _snsConfig;
        private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;
    
        public SNSFactory(
            IOptions<SNSConfig> snsConfig,
            IEnumerable<IAmazonSimpleNotificationService> snsServices
            )
        {
            _snsConfig = snsConfig.Value;
            _snsServices = snsServices;
        }
    
        public IAmazonSimpleNotificationService ForDefault()
        {
            return GetSNS(_snsConfig.SNSDefaultRegion);
        }
    
        public IAmazonSimpleNotificationService ForSMS()
        {
            return GetSNS(_snsConfig.SNSSMSRegion);
        }
    
        private IAmazonSimpleNotificationService GetSNS(string region)
        {
            return GetSNS(RegionEndpoint.GetBySystemName(region));
        }
    
        private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
        {
            IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);
    
            if (service == null)
            {
                throw new Exception($"No SNS service registered for region: {region}");
            }
    
            return service;
        }
    }
    
    public interface ISNSFactory
    {
        IAmazonSimpleNotificationService ForDefault();
    
        IAmazonSimpleNotificationService ForSMS();
    }
    

    Now you can get the SNS service for the region that you want in your custom service or controller

    public class SmsSender : ISmsSender
    {
        private readonly IAmazonSimpleNotificationService _sns;
    
        public SmsSender(ISNSFactory snsFactory)
        {
            _sns = snsFactory.ForSMS();
        }
    
        .......
     }
    
    public class DeviceController : Controller
    {
        private readonly IAmazonSimpleNotificationService _sns;
    
        public DeviceController(ISNSFactory snsFactory)
        {
            _sns = snsFactory.ForDefault();
        }
    
         .........
    }
    
    0 讨论(0)
  • 2020-11-22 11:03

    A factory approach is certainly viable. Another approach is to use inheritance to create individual interfaces that inherit from IService, implement the inherited interfaces in your IService implementations, and register the inherited interfaces rather than the base. Whether adding an inheritance hierarchy or factories is the "right" pattern all depends on who you speak to. I often have to use this pattern when dealing with multiple database providers in the same application that uses a generic, such as IRepository<T>, as the foundation for data access.

    Example interfaces and implementations:

    public interface IService 
    {
    }
    
    public interface IServiceA: IService
    {}
    
    public interface IServiceB: IService
    {}
    
    public IServiceC: IService
    {}
    
    public class ServiceA: IServiceA 
    {}
    
    public class ServiceB: IServiceB
    {}
    
    public class ServiceC: IServiceC
    {}
    

    Container:

    container.Register<IServiceA, ServiceA>();
    container.Register<IServiceB, ServiceB>();
    container.Register<IServiceC, ServiceC>();
    
    0 讨论(0)
  • 2020-11-22 11:03

    FooA, FooB and FooC implements IFoo

    Services Provider:

    services.AddTransient<FooA>(); // Note that there is no interface
    services.AddTransient<FooB>();
    services.AddTransient<FooC>();
    
    services.AddSingleton<Func<Type, IFoo>>(x => type =>
    {
        return (IFoo)x.GetService(type);
    });
    

    Destination:

    public class Test
    {
        private readonly IFoo foo;
    
        public Test(Func<Type, IFoo> fooFactory)
        {
            foo = fooFactory(typeof(FooA));
        }
    
        ....
    
    }
    

    If you want to change the FooA to FooAMock for test purposes:

    services.AddTransient<FooAMock>();
    
    services.AddSingleton<Func<Type, IFoo>>(x => type =>
    {
        if(type.Equals(typeof(FooA))
            return (IFoo)x.GetService(typeof(FooAMock));
        return null;
    });
    
    0 讨论(0)
  • I did a simple workaround using Func when I found myself in this situation.

    Firstly declare a shared delegate:

    public delegate IService ServiceResolver(string key);
    

    Then in your Startup.cs, setup the multiple concrete registrations and a manual mapping of those types:

    services.AddTransient<ServiceA>();
    services.AddTransient<ServiceB>();
    services.AddTransient<ServiceC>();
    
    services.AddTransient<ServiceResolver>(serviceProvider => key =>
    {
        switch (key)
        {
            case "A":
                return serviceProvider.GetService<ServiceA>();
            case "B":
                return serviceProvider.GetService<ServiceB>();
            case "C":
                return serviceProvider.GetService<ServiceC>();
            default:
                throw new KeyNotFoundException(); // or maybe return null, up to you
        }
    });
    

    And use it from any class registered with DI:

    public class Consumer
    {
        private readonly IService _aService;
    
        public Consumer(ServiceResolver serviceAccessor)
        {
            _aService = serviceAccessor("A");
        }
    
        public void UseServiceA()
        {
            _aService.DoTheThing();
        }
    }
    

    Keep in mind that in this example the key for resolution is a string, for the sake of simplicity and because OP was asking for this case in particular.

    But you could use any custom resolution type as key, as you do not usually want a huge n-case switch rotting your code. Depends on how your app scales.

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

    My solution for what it's worth... considered switching to Castle Windsor as can't say I liked any of the solutions above. Sorry!!

    public interface IStage<out T> : IStage { }
    
    public interface IStage {
          void DoSomething();
    }
    

    Create your various implementations

    public class YourClassA : IStage<YouClassA> { 
        public void DoSomething() 
        {
            ...TODO
        }
    }
    
    public class YourClassB : IStage<YourClassB> { .....etc. }
    

    Registration

    services.AddTransient<IStage<YourClassA>, YourClassA>()
    services.AddTransient<IStage<YourClassB>, YourClassB>()
    

    Constructor and instance usage...

    public class Whatever
    {
       private IStage ClassA { get; }
    
       public Whatever(IStage<YourClassA> yourClassA)
       {
             ClassA = yourClassA;
       }
    
       public void SomeWhateverMethod()
       {
            ClassA.DoSomething();
            .....
       }
    
    0 讨论(0)
  • 2020-11-22 11:06

    I know this post is a couple years old, but I keep running into this and I'm not happy with the service locator pattern.

    Also, I know the OP is looking for an implementation which allows you to choose a concrete implementation based on a string. I also realize that the OP is specifically asking for an implementation of an identical interface. The solution I'm about to describe relies on adding a generic type parameter to your interface. The problem is that you don't have any real use for the type parameter other than service collection binding. I'll try to describe a situation which might require something like this.

    Imagine configuration for such a scenario in appsettings.json which might look something like this (this is just for demonstration, your configuration can come from wherever you want as long as you have the correction configuration provider):

    {
      "sqlDataSource": {
        "connectionString": "Data Source=localhost; Initial catalog=Foo; Connection Timeout=5; Encrypt=True;",
        "username": "foo",
        "password": "this normally comes from a secure source, but putting here for demonstration purposes"
      },
      "mongoDataSource": {
        "hostName": "uw1-mngo01-cl08.company.net",
        "port": 27026,
        "collection": "foo"
      }
    }
    

    You really need a type that represents each of your configuration options:

    public class SqlDataSource
    {
      public string ConnectionString { get;set; }
      public string Username { get;set; }
      public string Password { get;set; }
    }
    
    public class MongoDataSource
    {
      public string HostName { get;set; }
      public string Port { get;set; }
      public string Collection { get;set; }
    }
    

    Now, I know that it might seem a little contrived to have two implementations of the same interface, but it I've definitely seen it in more than one case. The ones I usually come across are:

    1. When migrating from one data store to another, it's useful to be able to implement the same logical operations using the same interfaces so that you don't need to change the calling code. This also allows you to add configuration which swaps between different implementations at runtime (which can be useful for rollback).
    2. When using the decorator pattern. The reason you might use that pattern is that you want to add functionality without changing the interface and fall back to the existing functionality in certain cases (I've used it when adding caching to repository classes because I want circuit breaker-like logic around connections to the cache that fall back to the base repository -- this gives me optimal behavior when the cache is available, but behavior that still functions when it's not).

    Anyway, you can reference them by adding a type parameter to your service interface so that you can implement the different implementations:

    public interface IService<T> {
      void DoServiceOperation();
    }
    
    public class MongoService : IService<MongoDataSource> {
      private readonly MongoDataSource _options;
    
      public FooService(IOptionsMonitor<MongoDataSource> serviceOptions){
        _options = serviceOptions.CurrentValue
      }
    
      void DoServiceOperation(){
        //do something with your mongo data source options (connect to database)
        throw new NotImplementedException();
      }
    }
    
    public class SqlService : IService<SqlDataSource> {
      private readonly SqlDataSource_options;
    
      public SqlService (IOptionsMonitor<SqlDataSource> serviceOptions){
        _options = serviceOptions.CurrentValue
      }
    
      void DoServiceOperation(){
        //do something with your sql data source options (connect to database)
        throw new NotImplementedException();
      }
    }
    

    In startup, you'd register these with the following code:

    services.Configure<SqlDataSource>(configurationSection.GetSection("sqlDataSource"));
    services.Configure<MongoDataSource>(configurationSection.GetSection("mongoDataSource"));
    
    services.AddTransient<IService<SqlDataSource>, SqlService>();
    services.AddTransient<IService<MongoDataSource>, MongoService>();
    

    Finally in the class which relies on the Service with a different connection, you just take a dependency on the service you need and the DI framework will take care of the rest:

    [Route("api/v1)]
    [ApiController]
    public class ControllerWhichNeedsMongoService {  
      private readonly IService<MongoDataSource> _mongoService;
      private readonly IService<SqlDataSource> _sqlService ;
    
      public class ControllerWhichNeedsMongoService(
        IService<MongoDataSource> mongoService, 
        IService<SqlDataSource> sqlService
      )
      {
        _mongoService = mongoService;
        _sqlService = sqlService;
      }
    
      [HttpGet]
      [Route("demo")]
      public async Task GetStuff()
      {
        if(useMongo)
        {
           await _mongoService.DoServiceOperation();
        }
        await _sqlService.DoServiceOperation();
      }
    }
    

    These implementations can even take a dependency on each other. The other big benefit is that you get compile-time binding so any refactoring tools will work correctly.

    Hope this helps someone in the future.

    0 讨论(0)
提交回复
热议问题