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

105

让我们考虑在 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以修改服务注册?

5个回答

152

1
“Replace” 内部调用了我考虑过的 “Add” 和 “Remove” 方法。无论如何,这是一个很好的补充,谢谢。 - Ilya Chumakov
93
另一种方法(代码行数更少,但使用了相同的方法):services.Replace(ServiceDescriptor.Transient<IFoo, FooB>()); - Kolky
用单例模式来做这个是否是一个好的实践? - Gustavo Ferrufino
@Vesper 在某些情况下,您可能需要替换单例。 - Dustin Kingen
@Vesper 当然,在测试场景中非常有用(例如,您有一个单例记录器,并且您想要测试是否已写入某些日志) - Ohad Schneider
如果你想要一个单例:services.Replace(ServiceDescriptor.Transient<IFoo>(_ => yourInstance)); - DonSleza4e

68

如果您知道以下两个简单的方法,就可以轻松地覆盖ASP.NET Core DI功能:

1. ServiceCollection只是List<ServiceDescriptor>的包装器:

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

2. 当一个服务被注册时,会向列表中添加一个新描述符

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

因此,可以向该列表添加/删除描述符以替换注册:
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);

我们以Replace<TService, TImplementation>扩展方法结束:

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

它的实现方式:

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

8
我们如何将ServiceCollection的变化反映到IServiceProvider中? - lbrahim
1
我该如何在控制器中使用这个?我的意思是,我该如何从控制器中替换一个服务? - Hatef.
@Hatef,你不能从控制器中完成它,应该在“Startup.ConfigureServices”方法中完成。 - Ilya Chumakov
1
而修改 ServiceCollection 的唯一正确方法是在容器构建之前进行,即在创建实现 IServiceProvider 的对象之前。 - Ilya Chumakov

8

补充一下 @ilya-chumakov 的优秀答案,这是同样的方法,但支持实现工厂。

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

如果我们想要将其与实例化服务的工厂一起使用,可以像以下示例那样:

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

我尝试在控制器中使用您的代码,但它没有起作用。我错过了什么吗? - Hatef.
如果您提供更多有关不起作用的细节,也许我可以帮忙。但是这段代码运行良好。 - diegosasw

6
在最新版本的 .NET Core (net5.0) 中,应该是这样的。
using Microsoft.Extensions.DependencyInjection;

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

或者尝试这个。

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

4
第一个无效。第二个有效。 - lonix

3

我使用以下方式替换我的测试夹具WebApplicationFactory中的存储库

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

并在测试中使用它

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

如果服务有多个注册,那么您必须先删除。替换不会删除所有实例。

services.RemoveAll(typeof(IStoreRepository));

这个 builder.ConfigureTestServices() 对我的单元测试项目非常有帮助,谢谢。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接