29
votes

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)
5
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 saysTseng
It's not unit tests it's part of businesses logic =) TestSuite is business entityIlya
I posted an answer to a similar question here using a strongly typed approach: stackoverflow.com/questions/39174989/…Ciarán Bruen

5 Answers

29
votes

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");
    }
}
30
votes

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.

7
votes

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)
4
votes

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!

1
votes

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.