Replace service registration in ASP.NET Core built-in DI container?

46,579

Solution 1

This is simple using the Replace(IServiceCollection, ServiceDescriptor) method from the ServiceCollectionDescriptorExtensions class.

// IFoo -> FooA
services.AddTransient<IFoo, FooA>();

// Replace
// IFoo -> FooB
var descriptor =
    new ServiceDescriptor(
        typeof(IFoo),
        typeof(FooB),
        ServiceLifetime.Transient);
services.Replace(descriptor);

See also:

Solution 2

It is easy to override ASP.NET Core DI functionality if you know two simple things:

1. ServiceCollection is just a wrapper on top of List<ServiceDescriptor>:

    public class ServiceCollection : IServiceCollection
    {
        private List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
    }

2. When a service is registered, a new descriptor is added to list:

    private static IServiceCollection Add(
        IServiceCollection collection,
        Type serviceType,
        Type implementationType,
        ServiceLifetime lifetime)
    {
        var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
        collection.Add(descriptor);
        return collection;
    }

Therefore, it is possible to add/remove descriptors to/from this list to replace the registration:

IFoo service = services.BuildServiceProvider().GetService<IFoo>();
Assert.True(service is FooA);

var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IFoo));
Assert.NotNull(descriptor);
services.Remove(descriptor);

service = services.BuildServiceProvider().GetService<IFoo>();
Assert.Null(service);

We finish with Replace<TService, TImplementation> extention method:

services.Replace<IFoo, FooB>(ServiceLifetime.Transient);

Its implementation:

public static IServiceCollection Replace<TService, TImplementation>(
    this IServiceCollection services,
    ServiceLifetime lifetime)
    where TService : class
    where TImplementation : class, TService
{
    var descriptorToRemove = services.FirstOrDefault(d => d.ServiceType == typeof(TService));

    services.Remove(descriptorToRemove);

    var descriptorToAdd = new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime);

    services.Add(descriptorToAdd);

    return services;
}

Solution 3

Just to add on @ilya-chumakov 's great answer, here is the same method but with support for implementation factories

public static IServiceCollection Replace<TService>(
    this IServiceCollection services,
    Func<IServiceProvider, TService> implementationFactory,
    ServiceLifetime lifetime)
    where TService : class
{
    var descriptorToRemove = services.FirstOrDefault(d => d.ServiceType == typeof(TService));

    services.Remove(descriptorToRemove);

    var descriptorToAdd = new ServiceDescriptor(typeof(TService), implementationFactory, lifetime);

    services.Add(descriptorToAdd);

    return services;
}

in case we want to use it with a factory that instantiates the service like the following sample:

var serviceProvider = 
  new ServiceCollection()
    .Replace<IMyService>(sp => new MyService(), ServiceLifetime.Singleton)
    .BuildServiceProvider();

Solution 4

In the latest version of .net core(net5.0), this is how it should be.

using Microsoft.Extensions.DependencyInjection;

services.Replace<IFoo, FooB>(ServiceLifetime.Transient); // Or ServiceLifetime.Singleton

Or try this one.

services.Replace(ServiceDescriptor.Transient<IFoo, FooB>());
Share:
46,579

Related videos on Youtube

Ilya Chumakov
Author by

Ilya Chumakov

Clean code, travels, hiking, photography.

Updated on December 18, 2021

Comments

  • Ilya Chumakov
    Ilya Chumakov over 2 years

    Let us consider a service registration in Startup.ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IFoo, FooA>();
    }
    

    Is it possible to change IFoo registration to FooB after AddTransient has been called? It can be helpful for testing purposes (for example, in TestStartup subclass) or if our access to codebase is limited.

    If we register another IFoo implementation:

    services.AddTransient<IFoo, FooA>();
    services.AddTransient<IFoo, FooB>();
    

    Then GetService<IFoo> returns FooB instead of FooA:

    IFoo service = services.BuildServiceProvider().GetService<IFoo>();
    Assert.True(service is FooB);
    

    However, GetServices<IFoo> successfully returns both implementations (and the same for GetService<IEnumerable<IFoo>>):

    var list = services.BuildServiceProvider().GetServices<IFoo>().ToList();
    Assert.Equal(2, list.Count);
    

    There is Remove(ServiceDescriptor) method in IServiceCollection contract. What should I do with ServiceDescriptor to modify a service registration?

  • Ilya Chumakov
    Ilya Chumakov about 7 years
    Replace internally calls the same Add and Remove methods which I considered. It is a good addition anyway, thanks.
  • Kolky
    Kolky over 6 years
    Another way (in less lines, but using the same methods); services.Replace(ServiceDescriptor.Transient<IFoo, FooB>());
  • lbrahim
    lbrahim almost 5 years
    How can we reflect the change of ServiceCollection to IServiceProvider?
  • Gustavo Ferrufino
    Gustavo Ferrufino over 3 years
    Would it be a good practice to do this but with a singleton?
  • Dustin Kingen
    Dustin Kingen over 3 years
    @Vesper There may be use cases when you need to replace a singleton.
  • Hatef.
    Hatef. over 3 years
    How can i use this in a controller? I mean how can I replace a service from a controller?
  • Hatef.
    Hatef. over 3 years
    I tried to use your code in controller. but it doesn't work. do I missed something?
  • diegosasw
    diegosasw over 3 years
    If you provide more details about what's not working maybe I can help. But this code works fine.
  • Ilya Chumakov
    Ilya Chumakov over 3 years
    @Hatef you can't do it from a controller, it should be done from Startup.ConfigureServices method.
  • Ilya Chumakov
    Ilya Chumakov over 3 years
    And the only proper way to modify ServiceCollection is to do it before the container is built, i.e. before object implementing IServiceProvider is created.
  • Ohad Schneider
    Ohad Schneider over 3 years
    @Vesper certainly, very useful in test scenarios (e.g. you have a singleton logger and you want to test some logs are being written)
  • lonix
    lonix over 2 years
    The first one doesn't work. The second one works.