在 ASP.NET Core 内置的 DI 容器中替换服务注册?

让我们考虑一下 Startup.ConfigureServices中的服务注册:

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

是否有可能在 AddTransient被调用后将 IFoo注册改为 FooB?它可以有助于测试目的(例如,在 TestStartup子类) ,或者如果我们的访问代码库是有限的。

如果我们注册另一个 IFoo实现:

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

然后 GetService<IFoo>返回 FooB而不是 FooA:

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

然而,GetServices<IFoo>成功地返回了这两个实现(对于 GetService<IEnumerable<IFoo>>也是一样的) :

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

IServiceCollection契约中有 Remove(ServiceDescriptor)方法。我应该如何处理 ServiceDescriptor来修改服务注册?

55779 次浏览

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;
}

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:

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();

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>());

I replace the repository in my Test Fixture WebApplicationFactory using

    public static WebApplicationFactory<TEntryPoint> WithRepository<TEntryPoint>(
this WebApplicationFactory<TEntryPoint> webApplicationFactory,
IStoreRepository storeRespository)
where TEntryPoint : class
{
return webApplicationFactory.WithWebHostBuilder(builder => builder.ConfigureTestServices(
services =>
{
services.Replace(ServiceDescriptor.Scoped(p => storeRespository));
}));
}

And use it in the test as

var respository = new ListRepository();
var client = _factory
.WithRepository(respository)
.CreateClient();

If the services has multiple registrations, then you have to remove first. Replace is not removing all the instances.

services.RemoveAll(typeof(IStoreRepository));