在本文中,我描述了如何在 ASP.NET Core 中使用的 Microsoft.Extensions.DependencyInjection
容器中注册具有多个公共接口的具体类。 使用这种方法,您将能够使用具体类实现的任何接口来检索具体类。 例如,如果您有以下类:
public class MyTestClass: ISomeInterface, ISomethingElse { }
然后您将能够注入 ISomeInterface
或 ISomethingElse
,您将收到相同的 MyTestClass
实例。
以特定方式注册 MyTestClass
以避免意外的生命周期问题很重要,例如有两个单例实例!
在这篇文章中,我简要概述了 ASP.NET Core 中的 DI 容器及其与第三方容器相比的一些限制。 然后,我将描述将对多个接口的请求“转发”到具体类型的概念,以及如何使用 ASP.NET Core DI 容器实现此目的。
ASP.NET Core DI 容器本身不支持将实现注册为多个服务(有时称为“转发”)。相反,您必须手动将服务的解析委托给工厂函数,例如
services.AddSingleton<IFoo>(x=> x.GetRequiredService<Foo>())
ASP.NET Core 中的依赖注入
ASP.NET Core 的关键特性之一是它使用依赖注入 (DI)。 该框架是围绕“符合容器”的抽象设计的,它允许框架本身使用一个简单的容器,同时还允许您插入功能更丰富的第三方容器。
“一致容器”的想法并非没有争议——我建议阅读 Mark Seemann 的这篇关于将一致容器作为反模式的文章,或者这篇来自 SimpleInjector 团队的关于 ASP.NET Core DI 容器的文章。
为了让第三方容器尽可能简单地实现符合标准的容器,它公开了非常有限的 API。 对于给定的服务(例如 IFoo
),您可以定义实现它的具体类(例如 Foo
),以及它应该具有的生命周期(例如 Singleton
)。 在这方面有一些变体,您可以直接提供服务的实例,也可以提供工厂方法,但这是您所能得到的最复杂的方法。
相比之下,.NET 中的第三方 DI 容器通常会提供更高级的注册 API。 例如,许多 DI 容器公开了一个用于配置的“扫描”API,您可以在其中搜索程序集中的所有类型,并将它们添加到您的 DI 容器中。 以下是一个 Autofac
示例:
var dataAccess = Assembly.GetExecutingAssembly();
builder.RegisterAssemblyTypes(dataAccess) // find all types in the assembly
.Where(t => t.Name.EndsWith("Repository")) // filter the types
.AsImplementedInterfaces() // register the service with all its public interfaces
.SingleInstance(); // register the services as singletons
在此示例中,Autofac
将在程序集中查找名称以“Repository
”结尾的所有具体类,并根据它们实现的任何公共接口将它们注册到容器中。 因此,例如,给定以下类和接口:
public interface IStubRepository {}
public interface ICachingRepository {}
public class StubRepository : IStubRepository {}
public class MyRepository : ICachingRepository {}
前面的Autofac
代码相当于手动在Startup.ConfigureServices
中的ASP.NET Core容器中注册这两个类及其各自的接口:
services.AddSingleton<IStubRepository, StubRepository>();
services.AddSingleton<ICachingRepository, MyRepository>();
但是如果一个类实现了多个接口会怎样呢?
将单个实现注册为多个服务
实现多个接口的类很常见,例如:
public interface IBar {}
public interface IFoo {}
public class Foo : IFoo, IBar {}
让我们编写一个快速测试,看看如果我们使用 ASP.NET Core DI 容器针对两个接口注册类会发生什么:
[Fact]
public void WhenRegisteredAsSeparateSingleton_InstancesAreNotTheSame()
{
var services = new ServiceCollection();
services.AddSingleton<IFoo, Foo>();
services.AddSingleton<IBar, Foo>();
var provider = services.BuildServiceProvider();
var foo1 = provider.GetService<IFoo>(); // An instance of Foo
var foo2 = provider.GetService<IBar>(); // An instance of Foo
Assert.Same(foo1, foo2); // FAILS
}
我们将 Foo
注册为 IFoo
和 IBar
的单例,但结果可能不是您所期望的。 我们实际上有两个 Foo
“Singleton
”实例,各自用于它注册的每个服务。
转发服务请求
针对多个服务注册一个实现的一般模式是一种常见的模式。 大多数第三方 DI 容器都内置了这个概念。例如:
Autofac
默认使用此行为 - 之前的测试会通过Windsor
具有“转发类型”的概念,允许您将多个服务“转发”到单个实现StructureMap
(现已停用)具有类似的“转发”类型概念。 据我所知,它的继任者Lamar
还没有,但我在这一点上可能是错的。
鉴于此要求非常普遍,ASP.NET Core DI 容器不可能实现这一点似乎很奇怪。 这个问题是在 2 年前提出的(由 David Fowler 提出),但已关闭。 幸运的是,有几个概念上简单但有些不雅的解决方案。
1.提供服务实例(仅限单例)
最简单的方法是在注册服务时提供 Foo
的实例。 每个注册的服务都将返回您在请求时提供的确切实例,确保只有一个实例。
[Fact]
public void WhenRegisteredAsInstance_InstancesAreTheSame()
{
var foo = new Foo(); // The singleton instance
var services = new ServiceCollection();
services.AddSingleton<IFoo>(foo);
services.AddSingleton<IBar>(foo);
var provider = services.BuildServiceProvider();
var foo1 = provider.GetService<IFoo>();
var foo2 = provider.GetService<IBar>();
Assert.Same(foo1, foo); // PASSES;
Assert.Same(foo2, foo); // PASSES;
}
对此有一个很大的警告——您必须能够在配置时实例化 Foo
,并且您必须知道并提供它的所有依赖项。 这在某些情况下可能对您有用,但不是很灵活。
此外,您只能使用这种方法来注册单例。 如果您希望 Foo
成为每个请求范围(Scoped
)的单个实例,那么您就不走运了。 相反,您需要使用以下技术。
2.使用工厂方法实现转发
如果我们分解我们的需求,就会出现一个替代解决方案:
- 我们希望我们注册的服务(
Foo
)有特定的生命周期(例如Singleton
或Scoped
) - 当请求
IFoo
时,返回Foo
的实例 - 请求
IBar
时,也返回Foo
的实例
根据这三个规则,我们可以编写另一个测试:
[Fact]
public void WhenRegisteredAsForwardedSingleton_InstancesAreTheSame()
{
var services = new ServiceCollection();
services.AddSingleton<Foo>(); // 我们必须显式注册 Foo
services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>()); // 将请求转发给 Foo
services.AddSingleton<IBar>(x => x.GetRequiredService<Foo>()); // 将请求转发给 Foo
var provider = services.BuildServiceProvider();
var foo1 = provider.GetService<Foo>(); // Foo 的一个实例
var foo2 = provider.GetService<IFoo>(); // Foo 的一个实例
var foo3 = provider.GetService<IBar>(); // Foo 的一个实例
Assert.Same(foo1, foo2); // 通过
Assert.Same(foo1, foo3); // 通过
}
为了将对接口的请求“转发”到具体类型,您必须做两件事:
- 使用
services.AddSingleton<Foo>()
显式注册具体类型 - 通过提供工厂函数将接口请求委托给具体类型:
services.AddSingleton<IFoo>(x =>x.GetRequiredService<Foo>())
使用这种方法,无论您请求哪个实现的服务,您都将拥有一个真正的 Foo
单例实例。
这种提供“转发”类型的方法已在原始问题中指出,并有一个警告——它不是很有效。通常最好尽可能避免使用“服务定位器样式”
GetService()
调用。 但是,我觉得在这种情况下这绝对是更可取的做法。
概括
在这篇文章中,我描述了如果将具体类型注册为 ASP.NET Core DI 服务的多个服务会发生什么。 特别是,我展示了如何最终得到 Singleton
对象的多个副本,这可能会导致细微的错误。 要解决这个问题,您可以在注册时提供服务实例,也可以使用工厂方法委托服务解析。 使用工厂方法不是很有效,但通常是最好的方法。