IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()
而且,在实例化需要其构造函数中的 ILoggingFactory 的 PaymentService 类时,DI 将负责检索具体的 ILoggingFactory 实例并利用该实例。
如果 TService 类型中没有此类方法可用,则可以重载 AddSingleton 扩展方法,该方法采用了 Func<IServiceProvider, TService> implementationFactory(用于实例化 TService 的工厂方法)类型的委托。无论你是否提供工厂方法,服务收集实现都会确保将仅创建一个 TService 类型的实例,从而确保存在单一实例。在第一次调用触发 TService 实例的 GetService 后,在服务收集的生存期内将始终返回同一实例。
IServiceCollection 还包括 AddTransient(Type serviceType, Type implementationType) 和 AddTransient(Type serviceType, Func<IServiceProvider, TService> implementationFactory) 扩展方法。这些方法类似于 AddSingleton,不同的是每次调用这些方法时都会返回一个新实例,从而确保你始终拥有 TService 类型的新实例。
最后,有几个 AddScoped 类型的扩展方法。这些方法设计为在给定的上下文中返回同一实例,并且每当上下文(也称为作用域)更改时都会创建新实例。从概念上讲,ASP.NET Core 的行为映射到作用域生存期。从本质上讲,新实例是针对每个 HttpContext 实例创建的,而且每当在相同的 HttpContext 内调用 GetService 时,都会返回完全相同的 TService 实例。
总之,有四个生存期选项,用于从服务收集实现返回的对象: Instance、Singleton、Transient 和 Scoped。最后三个是在 ServiceLifetime 枚举中定义的 (bit.ly/1SFtcaG)。但是,缺少 Instance,因为它是 Scoped(在其中无法更改上下文)的特殊用例。
之前我提到过 ServiceCollection 在概念上就像一个名称/值对,它将 TService 类型用于查找。ServiceCollection 类型的实际实现在 ServiceDescription 类中完成(请参阅 bit.ly/1SFoDgu)。该类为实例化 TService(即,ServiceType (TService))、ImplementationType 或 ImplementationFactory 委托以及 ServiceLifetime 所需的信息提供了一个容器。除了 ServiceDescriptor 构造函数,ServiceDescriptor 上还有许多静态工厂方法,可帮助实例化 ServiceDescriptor 本身。
无论使用哪种生存期注册 TService,TService 本身必须是一个引用类型,而不是值类型。每当你将类型参数用于 TService(而不是作为参数传递 Type)时,编译器都会使用泛型类约束进行验证。然而,编译器不会验证是否使用的是对象类型 TService。你一定要避免这种情况,以及任何其他非独特的接口(或许如 IComparable)。原因是,如果你注册了对象类型的内容,无论你在 GetService 调用中指定哪种类型的 TService,将始终返回注册为 TService 类型的对象。
DI 实现的依赖关系注入
ASP.NET 利用 DI 的程度之深,事实上,你可以在 DI 框架本身内实现 DI。换句话说,你不限于使用在 Microsoft.Extensions.DependencyInjection 中发现的 DI 机制的 ServiceCollection 实现。相反,只要你有实现 IServiceCollection(在 Microsoft.Extensions.DependencyInjection.Abstractions 中定义,请参阅 bit.ly/1SKdm1z)或IServiceProvider(在 .NET Core lib 框架的 System 命名空间内定义)的类,你就可以替代自己的 DI 框架或利用另外一个完善的 DI 框架,其中包括 Ninject(ninject.org,经过数年的努力维护 @IanfDavis 呼之欲出)和 Autofac (autofac.org)。
浅谈 ActivatorUtilities
Microsoft.Framework.DependencyInjection.Abstractions 还包括静态帮助程序类,该类提供了一些有用的方法,用于处理未使用 IServiceProvider(自定义的 ObjectFactory 委托)注册的构造函数参数,或者在想要创建默认实例的情况下,调用 GetService 时返回 null。你可以找到一些在 MVC 框架和 SignalR 库中使用此实用工具类的示例。在第一种情况下,存在一个带有 CreateInstance<T>(IServiceProvider provider, params object[] parameters) 签名的方法,允许你针对未注册的参数使用 DI 框架将构造函数参数传入到注册的类型中。你可能还会有性能需求,lambda 函数需要生成已编译的 lambda 类型。返回 ObjectFactory 的 CreateFactory(Type instanceType, Type[] argumentTypes) 方法在这种情况下可能有用。第一个参数是用户寻求的类型,而第二个参数是所有的构造函数类型,以匹配你希望使用的第一个类型的构造函数。在其实现中,这些片段都精简到已编译的 lambda,多次调用后,性能会相当高。最后,GetServiceOrCreateInstance<T>(IServiceProvider provider) 方法提供了一个简单方式,用于提供可能已选择在其他地方注册的类型的默认实例。这在调用之前允许 DI 的情况下尤为有用,但是,如果未发生这种情况,你会获得一个回退实现。
与 .NET Core 日志记录和配置一样,.NET Core DI 机制提供了一个相对简单的功能实现。虽然你不可能找到其他一些框架的更高级的 DI 功能,但 .NET Core 版本是轻量级的,并且是一个很好的入门方式。此外(再如日志记录和配置),.NET Core 实现可以被一个更成熟的实现替代。因此,你可能会考虑利用 .NET Core DI 框架作为一个“包装器”,通过它,将来你可以根据需要插入其他 DI 框架。通过这种方式,你不必定义自己的“自定义”DI 包装器,但可以利用 .NET Core 的包装器作为标准,任何客户端/应用程序都可以为标准的包装器插入自定义的实现。
关于 ASP.NET Core 需要注意的是,它自始至终都在利用 DI。这无疑是一个重大实践,在单元测试中尝试替代库的模拟实现时,如果你需要它,它会尤为重要。缺点是,并非简单的调用带有 new 运算符的构造函数,DI 注册和 GetService 调用的复杂性是必要的。我不禁想知道,C# 语言是否可以简化这种复杂性,但是,基于目前的 C# 7.0 设计,要实现这一点并不容易。
Mark Michaelis是 IntelliTect 的创始人,担任首席技术架构师和培训师。在近二十年的时间里,他一直是 Microsoft MVP,并且自 2007 年以来一直担任 Microsoft 区域总监。Michaelis 还是多个 Microsoft 软件设计评审团队(包括 C#、Microsoft Azure、SharePoint 和 Visual Studio ALM)的成员。他在开发者会议上发表了演讲,并撰写了大量书籍,包括最新的“必备 C# 6.0(第 5 版)”(itl.tc/EssentialCSharp)。可通过他的 Facebook facebook.com/Mark.Michaelis、博客 IntelliTect.com/Mark、Twitter @markmichaelis 或电子邮件 mark@IntelliTect.com 与他取得联系。
感谢以下 IntelliTect 技术专家对本文的审阅: Kelly Adams、Kevin Bost、Ian Davis 和 Phil Spokas