How to register multiple implementations of the sa

2019-01-03 12:24发布

I have services that are derived from same interface

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

Typically other IOC containers like Unity allow you to register concrete implementations by some Key that distinguishes them.

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

I don't see any of the Add Service method takes key or name parameter that typically used to distinguish the concrete implementation.

    public void ConfigureServices(IServiceCollection services)
    {            
         // How do I register services here of the same interface            
    }


    public MyController:Controller
    {
       public void DoSomeThing(string key)
       { 
          // How do get service based on key
       }
    }

Is the Factory pattern the only option here?

Update1
I have gone though the article here that shows how to use factory pattern to get service instances when we have multiple concreate implementation. However it is still not complete solution. when I call _serviceProvider.GetService() method I cannot inject data into constructor. For example consider this example

public class ServiceA : IService
{
     private string _efConnectionString;
     ServiceA(string efconnectionString)
     {
       _efConnecttionString = efConnectionString;
     } 
}

public class ServiceB : IService
{    
   private string _mongoConnectionString;
   public ServiceB(string mongoConnectionString)
   {
      _mongoConnectionString = mongoConnectionString;
   }
}

public class ServiceC : IService
{    
    private string _someOtherConnectionString
    public ServiceC(string someOtherConnectionString)
    {
      _someOtherConnectionString = someOtherConnectionString;
    }
}

How can _serviceProvider.GetService() inject appropriate connection string? In Unity or any other IOC we can do that at the time of type registration. I can use IOption however that will require me to inject all settings, I cannot inject a particular connectionstring into the service.

Also note that I am trying to avoid using other containers (including Unity) because then I have to register everything else ( eg Controllers) with new container as well.

Also using factory pattern to create service instance is against DIP as factory increases the number of dependencies a client is forced to depend upon details here

So I think the default DI in ASP.NET core missing 2 things
1>Register instances using key
2>Inject static data into constructor during registration

14条回答
beautiful°
2楼-- · 2019-01-03 12:51

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.

查看更多
萌系小妹纸
3楼-- · 2019-01-03 12:58

Bit late to this party, but here is my solution:...

Startup.cs or Program.cs if Generic Handler...

services.AddTransient<IMyInterface<CustomerSavedConsumer>, CustomerSavedConsumer>();
services.AddTransient<IMyInterface<ManagerSavedConsumer>, ManagerSavedConsumer>();

IMyInterface of T Interface Setup

public interface IMyInterface<T> where T : class, IMyInterface<T>
{
    Task Consume();
}

Concrete implementations of IMyInterface of T

public class CustomerSavedConsumer: IMyInterface<CustomerSavedConsumer>
{
    public async Task Consume();
}

public class ManagerSavedConsumer: IMyInterface<ManagerSavedConsumer>
{
    public async Task Consume();
}

Hopefully if there is any issue with doing it this way, someone will kindly point out why this is the wrong way to do this.

查看更多
Lonely孤独者°
4楼-- · 2019-01-03 12:59

While the out of the box implementation doesn't offer it, here's a sample project that allows you to register named instances, and then inject INamedServiceFactory into your code and pull out instances by name. Unlike other facory solutions here, it will allow you to register multiple instances of same implementation but configured differently

https://github.com/macsux/DotNetDINamedInstances

查看更多
ら.Afraid
5楼-- · 2019-01-03 13:01

I did something similar sometime ago, and I think that the implementation was nice.

Let's say that you have to process incoming messages, each message has a type, so the common approach here it's to use a switch and do things like this:

@Override
public void messageArrived(String type, Message message) throws Exception {
    switch(type) {
        case "A": do Something with message; break;
        case "B": do Something else with message; break;
        ...
    }
}

We can avoid that switch code, and also we can avoid the need to modify that code each time that a new type is added, following this pattern:

@Override
public void messageArrived(String type, Message message) throws Exception {
     messageHandler.getMessageHandler(type).handle(message);
}

This is nice right? So how we can achieve something like that?

First let's define an annotation and also an interface

@Retention(RUNTIME)
@Target({TYPE})
public @interface MessageHandler {
     String value()
}

public interface Handler {
    void handle(MqttMessage message);
}

and now let's use this annotation in a class:

@MessageHandler("myTopic")
public class MyTopicHandler implements Handler {

    @override
    public void handle(MqttMessage message) {
        // do something with the message
    }
}

The last part it's to write the MessageHandler class, in this class you just need to store in a map all your handlers instances, the key of the map will be the topic name and the value and instance of the class. Basically you getMessageHandler method will look something like:

public Handler getMessageHandler(String topic) {
    if (handlerMap == null) {
        loadClassesFromClassPath(); // This method should load from classpath all the classes with the annotation MessageHandler and build the handlerMap
    }
    return handlerMap.get(topic);
}

The good thing with this solution it's that if you need to add a new handler for a new message, you just need to write the new class, add the annotation to the class and that's all. The rest of the code remains the same because it's generic and it's using reflection to load and create instances.

Hope this helps you a bit.

查看更多
再贱就再见
6楼-- · 2019-01-03 13:04

I just simply inject an IEnumerable

ConfigureServices in Startup.cs

Assembly.GetEntryAssembly().GetTypesAssignableFrom<IService>().ForEach((t)=>
                {
                    services.AddScoped(typeof(IService), t);
                });

Services Folder

public interface IService
{
    string Name { get; set; }
}

public class ServiceA : IService
{
    public string Name { get { return "A"; } }
}

public class ServiceB : IService
{    
    public string Name { get { return "B"; } }
}

public class ServiceC : IService
{    
    public string Name { get { return "C"; } }
}

MyController.cs

public class MyController
{
    private readonly IEnumerable<IService> _services;
    public MyController(IEnumerable<IService> services)
    {
        _services = services;
    }
    public void DoSomething()
    {
        var service = _services.Where(s => s.Name == "A").Single();
    }
...
}

Extensions.cs

    public static List<Type> GetTypesAssignableFrom<T>(this Assembly assembly)
    {
        return assembly.GetTypesAssignableFrom(typeof(T));
    }
    public static List<Type> GetTypesAssignableFrom(this Assembly assembly, Type compareType)
    {
        List<Type> ret = new List<Type>();
        foreach (var type in assembly.DefinedTypes)
        {
            if (compareType.IsAssignableFrom(type) && compareType != type)
            {
                ret.Add(type);
            }
        }
        return ret;
    }
查看更多
够拽才男人
7楼-- · 2019-01-03 13:05

You're correct, the built in ASP.NET Core container does not have the concept of registering multiple services and then retrieving a specific one, as you suggest, a factory is the only real solution in that case.

Alternatively, you could switch to a third party container like Unity or StructureMap that does provide the solution you need (documented here: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing-the-default-services-container).

查看更多
登录 后发表回答