42

How can I inject different implementation of object for a specific class?

For example, in Unity, I can define two implementations of IRepository

container.RegisterType<IRepository, TestSuiteRepositor("TestSuiteRepository");
container.RegisterType<IRepository, BaseRepository>(); 

and call the needed implementation

public BaselineManager([Dependency("TestSuiteRepository")]IRepository repository)
Pang
  • 9,564
  • 146
  • 81
  • 122
Ilya Sulimanov
  • 7,636
  • 6
  • 47
  • 68
  • You shouldn't need or use IoC in unit tests (sign that you doing something **very wrong**). For Integration tests, you should use multiple startup classes like radu-matei says – Tseng Aug 22 '16 at 06:31
  • 4
    It's not unit tests it's part of businesses logic =) TestSuite is business entity – Ilya Sulimanov Aug 22 '16 at 06:35
  • I posted an answer to a similar question here using a strongly typed approach: https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core/59067353#59067353 – Ciarán Bruen Nov 27 '19 at 10:53
  • Does this answer your question? [How to register multiple implementations of the same interface in Asp.Net Core?](https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core) – Darrell Mar 12 '20 at 00:39

6 Answers6

40

As @Tseng pointed, there is no built-in solution for named binding. However using factory method may be helpful for your case. Example should be something like below:

Create a repository resolver:

public interface IRepositoryResolver
{
    IRepository GetRepositoryByName(string name);
}

public class RepositoryResolver : IRepositoryResolver 
{
    private readonly IServiceProvider _serviceProvider;
    public RepositoryResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public IRepository GetRepositoryByName(string name)
    {
         if(name == "TestSuiteRepository") 
           return _serviceProvider.GetService<TestSuiteRepositor>();
         //... other condition
         else
           return _serviceProvider.GetService<BaseRepositor>();
    }

}

Register needed services in ConfigureServices.cs

services.AddSingleton<IRepositoryResolver, RepositoryResolver>();
services.AddTransient<TestSuiteRepository>();
services.AddTransient<BaseRepository>(); 

Finally use it in any class:

public class BaselineManager
{
    private readonly IRepository _repository;

    public BaselineManager(IRepositoryResolver repositoryResolver)
    {
        _repository = repositoryResolver.GetRepositoryByName("TestSuiteRepository");
    }
}
adem caglin
  • 22,700
  • 10
  • 58
  • 78
  • 1
    I try waht u said, but in `GetRepositoryByName()` method, i got this error: `he non-generic method 'IServiceProvider.GetService(Type)' cannot be used with type arguments`! – pmn Jan 14 '17 at 11:20
  • 2
    You need Microsoft.Extensions.DependencyInjection or _serviceProvider.GetService(typeof(TestSuiteRepository)) – MorgoZ Sep 13 '17 at 16:50
  • This solution worked for me since I needed a DbContextFactory object. – Richard Dec 09 '17 at 21:45
  • 15
    This is the [service locator anti-pattern](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/) under the hood. Have a look at [this good explanation of service locator vs abstract factory](https://blog.ploeh.dk/2010/11/01/PatternRecognitionAbstractFactoryorServiceLocator/). You "should" never reference the DI container in your business layer. Instead pass a delegate with the parameter(s) needed by the container to get the instance you want. [Here](https://stackoverflow.com/a/31956803/2169965) is an example – gfache Jul 16 '19 at 15:34
  • @gfache - "should never reference the DI container in your business layer": how broad do you mean by "DI container"? I agree that injecting / having a dependency on the `IServiceProvider` is clearly a service locator pattern, but I've written business logic to include helper extension methods on `IServiceCollection` to keep registration logic near the definitions. Would you also consider this bad practice? – nicholas Jan 22 '22 at 16:35
  • @nicholas `IServiceCollection` is used at the registration stage and does not directly play a role in the dependency injection itself. I would say without having seen the code that this is a different case (read question). – gfache Mar 15 '22 at 10:34
34

In addition to @adem-caglin answer I'd like to post here some reusable code I've created for name-based registrations.

UPDATE Now it's available as nuget package.

In order to register your services you'll need to add following code to your Startup class:

        services.AddTransient<ServiceA>();
        services.AddTransient<ServiceB>();
        services.AddTransient<ServiceC>();
        services.AddByName<IService>()
            .Add<ServiceA>("key1")
            .Add<ServiceB>("key2")
            .Add<ServiceC>("key3")
            .Build();

Then you can use it via IServiceByNameFactory interface:

public AccountController(IServiceByNameFactory<IService> factory) {
    _service = factory.GetByName("key2");
}

Or you can use factory registration to keep the client code clean (which I prefer)

_container.AddScoped<AccountController>(s => new AccountController(s.GetByName<IService>("key2")));

Full code of the extension is in github.

neleus
  • 2,230
  • 21
  • 36
  • 5
    Seems like that defeats the purpose of dependency injection. Now your regular classes take a dependency on the dependency framework. – mac10688 Oct 16 '18 at 15:17
  • As an alternative the IoC container can decide what instance to inject. Although it requres more complex name-based rules on registrations that can be a good solution without explicit dependency. – neleus Nov 12 '18 at 12:26
  • 1
    @mac10688 finally I've added that possibility https://github.com/yuriy-nelipovich/DependencyInjection.Extensions – neleus Dec 04 '19 at 15:04
  • What's interesting is that by default, ASP.NET Core's default IOC implementation creates an IEnumerable of all instances of your implementations that you define in Startup. – Charles Owen Feb 08 '20 at 00:04
  • 1
    BTW, kudos on the library. I was able to plug it in very easily. I like the fact that it doesn't require any complicated wireup. I found the quickest object instantiation occurred when I merely ran a simple LINQ query against the IEnumerable that's created by default in startup. Ninject provided a Named Implementation and I don't see why Microsoft didn't just port something like that. I don't see how it violates the principles of IOC at all. Good job! – Charles Owen Feb 08 '20 at 00:16
  • @neleus, when using the Nelius package, how might I also pass in a constructor parameter if needed? For example if I want to have a fully defined database connection passed to a controller? Example of my attempt having a database HanaDb that inplements a database interface: builder.Services.AddScoped( _ => new HanaDb(HanaConnectionString)); builder.Services.AddByName() .Add("hanaDb"); – David Mays Feb 28 '22 at 18:31
  • 1
    Passing parameters is not possible by design. `.Add("hanaDb");` doesn't work because the factory is not responsible of dependency instantiation. It just resolves it from the aspnet.core IoC container. So if you want to pass constructor argument you must pass it to the IoC container with its registration. – neleus Aug 20 '22 at 22:09
10

You can't with the built-in ASP.NET Core IoC container.

This is by design. The built-in container is intentionally kept simple and easily extensible, so you can plug third-party containers in if you need more features.

You have to use a third-party container to do this, like Autofac (see docs).

public BaselineManager([WithKey("TestSuiteRepository")]IRepository repository)
Pang
  • 9,564
  • 146
  • 81
  • 122
Tseng
  • 61,549
  • 15
  • 193
  • 205
5

After having read the official documentation for dependency injection, I don't think you can do it in this way.

But the question I have is: do you need these two implementations at the same time? Because if you don't, you can create multiple environments through environment variables and have specific functionality in the Startup class based on the current environment, or even create multiple Startup{EnvironmentName} classes.

When an ASP.NET Core application starts, the Startup class is used to bootstrap the application, load its configuration settings, etc. (learn more about ASP.NET startup). However, if a class exists named Startup{EnvironmentName} (for example StartupDevelopment), and the ASPNETCORE_ENVIRONMENT environment variable matches that name, then that Startup class is used instead. Thus, you could configure Startup for development, but have a separate StartupProduction that would be used when the app is run in production. Or vice versa.

I also wrote an article about injecting dependencies from a JSON file so you don't have to recompile the entire application every time you want to switch between implementations. Basically, you keep a JSON array with services like this:

"services": [
    {
        "serviceType": "ITest",
        "implementationType": "Test",
        "lifetime": "Transient"
    }
]

Then you can modify the desired implementation in this file and not have to recompile or change environment variables.

Hope this helps!

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
radu-matei
  • 3,469
  • 1
  • 29
  • 47
  • 1
    And what should one do if we **need** two implementations at the same time? – VMAtm Apr 03 '17 at 16:53
  • 2
    Stacking multiple implementations of an interface seems like such a common use case that it hurts my head trying to figure out why they left this out. But that's Microsoft for you. – Jacobs Data Solutions Oct 03 '17 at 12:44
2

First up, this is probably still a bad idea. What you're trying to achieve is a separation between how the dependencies are used and how they are defined. But you want to work with the dependency injection framework, instead of against it. Avoiding the poor discover-ability of the service locator anti-pattern. Why not use generics in a similar way to ILogger<T> / IOptions<T>?

public BaselineManager(RepositoryMapping<BaselineManager> repository){
   _repository = repository.Repository;
}

public class RepositoryMapping<T>{
    private IServiceProvider _provider;
    private Type _implementationType;
    public RepositoryMapping(IServiceProvider provider, Type implementationType){
        _provider = provider;
        _implementationType = implementationType;
    }
    public IRepository Repository => (IRepository)_provider.GetService(_implementationType);
}

public static IServiceCollection MapRepository<T,R>(this IServiceCollection services) where R : IRepository =>
    services.AddTransient(p => new RepositoryMapping<T>(p, typeof(R)));

services.AddScoped<BaselineManager>();
services.MapRepository<BaselineManager, BaseRepository>();

Since .net core 3, a validation error should be raised if you have failed to define a mapping.

Jeremy Lakeman
  • 9,515
  • 25
  • 29
1

>= .NET 8: Keyed DI services

With .NET 8 we get an update of the extension libraries which brings keyed DI services so there is now a built-in solution.

Here the Microsoft documentation quoted from What's new in .NET 8? - Keyed DI services:


Keyed DI services

Keyed dependency injection (DI) services provides a means for registering and retrieving DI services using keys. By using keys, you can scope how your register and consume services. These are some of the new APIs:

The following example shows you to use keyed DI services.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<BigCacheConsumer>();
builder.Services.Addsingleton<SmallCacheConsumer>();

builder.Services.AddKeyedSingleton<IMemoryCache, BigCache>("big");
builder.Services.AddKeyedSingleton<IMemoryCache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());

app.Run();

class BigCacheConsumer([FromKeyedServices("big")] IMemoryCache cache)
{
    public object? GetData() => cache.Get("data");
}

class SmallCacheConsumer(IKeyedServiceProvider keyedServiceProvider)
{
    public object? GetData() => keyedServiceProvider.GetRequiredKeyedService<IMemoryCache>("small");
}

For more information, see dotnet/runtime#64427.


Mushroomator
  • 6,516
  • 1
  • 10
  • 27