ASP.NET Core 6框架揭秘实例演示[05]:依赖注入基本编程模式
毫不夸张地说,整个ASP.NET Core就是建立在依赖注入框架之上的。ASP.NET Core应用在启动时构建管道所需的服务,以及管道处理请求使用到的服务,均来源于依赖注入容器。依赖注入容器不仅为ASP.NET Core框架自身提供必要的服务,还为应用程序提供服务,依赖注入已经成为ASP.NET Core应用的基本编程模式。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[301]普通服务的注册和提取(源代码)
[302]针对泛型服务类型的支持(源代码)
[303]为同一类型提供多个服务注册(源代码)
[304]服务实例的生命周期(源代码)
[305]服务实例的释放回收(源代码)
[306]服务范围的验证(源代码)
[307]服务注册有效性的验证(源代码)
[301]普通服务的注册和提取
我们提供的演示实例是一个控制台程序。在添加了“Microsoft.Extensions.DependencyInjection”NuGet包引用之后,我们定义了如下接口和实现类型来表示相应的服务。如代码片段所示,Foo、Bar和Baz分别实现了对应的接口IFoo、IBar与IBaz。它们派生的基类Base实现了IDisposable接口,我们在其构造函数和实现的Dispose方法中打印出相应的文字以确定服务实例被创建和释放的时机。我们还定义了泛型的接口IFoobar<T1, T2>和对应的实现类Foobar<T1, T2>,后面讲用它们来演示针对泛型服务实例的提供。
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IFoobar<T1, T2> {} public class Base : IDisposable { public Base() => Console.WriteLine($"An instance of {GetType().Name} is created."); public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed."); } public class Foo : Base, IFoo, IDisposable { } public class Bar : Base, IBar, IDisposable { } public class Baz : Base, IBaz, IDisposable { } public class Foobar<T1, T2>: IFoobar<T1,T2> { public T1 Foo { get; } public T2 Bar { get; } public Foobar(T1 foo, T2 bar) { Foo = foo; Bar = bar; } }
在如下所示的演示程序中,我们创建了一个ServiceCollection对象(ServiceCollection实现了IServiceCollection接口),并现有调用AddTransient、AddScoped和AddSingleton扩展方法针对IFoo、IBar和IBaz接口注册了对应的服务,从方法命名可以看出注册的服务采用的生命周期模式分别为Transient、Scoped和Singleton。我们接下来调用IServiceCollection对象的BuildServiceProvider扩展方法创建出代表依赖注入容器的IServiceProvider对象,并调用它的GetService<T>扩展方法来提供所需的服务实例。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; var provider = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar>(_ => new Bar()) .AddSingleton<IBaz, Baz>() .BuildServiceProvider(); Debug.Assert(provider.GetService<IFoo>() is Foo); Debug.Assert(provider.GetService<IBar>() is Bar); Debug.Assert(provider.GetService<IBaz>() is Baz);
[302]针对泛型服务类型的支持
表示依赖注入容器的IServiceProvider对象还能提供泛型服务实例。如下面的代码片段所示,在为创建的ServiceCollection对象添加了针对IFoo和IBar接口的服务注册之后,我们调用AddTransient方法注册了针对泛型定义IFoobar<,>的服务(实现的类型为Foobar<,>)。在构建出代表依赖注入容器的IServiceProvider对象之后,我们利用它提供一个类型为IFoobar<IFoo, IBar>的服务实例(S302)。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; var provider = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) .BuildServiceProvider(); var foobar = (Foobar<IFoo, IBar>?)provider.GetService<IFoobar<IFoo, IBar>>(); Debug.Assert(foobar?.Foo is Foo); Debug.Assert(foobar?.Bar is Bar);
[303]为同一类型提供多个服务注册
我们可以为同一个类型添加多个服务注册,虽然所有服务注册均是有效的,但是GetService<T>扩展方法只能返回一个服务实例。框架采用了“后来居上”的策略,总是采用最近添加的服务注册来创建服务实例。GetServices<TService>扩展方法将利用指定服务类型的所有服务注册来提供一组服务实例。需要的演示程序添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们将Base作为泛型参数调用了GetServices<Base>方法,返回的集合将包含这三个类型的对象。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Linq; var services = new ServiceCollection() .AddTransient<Base, Foo>() .AddTransient<Base, Bar>() .AddTransient<Base, Baz>() .BuildServiceProvider() .GetServices<Base>(); Debug.Assert(services.OfType<Foo>().Any()); Debug.Assert(services.OfType<Bar>().Any()); Debug.Assert(services.OfType<Baz>().Any());
[304]服务实例的生命周期
代表依赖注入容器的IServiceProvider对象之间的层次结构促成了服务实例的三种生命周期模式。具体来说,由于Singleton服务实例保存在作为根容器的IServiceProvider对象上,所以能够在多个同根IServiceProvider对象之间提供真正的单例保证。Scoped服务实例被保存在当前服务范围对应的IServiceProvider对象上,所以只能在当前服务范围内保证提供的实例是单例的。对应类型没有实现IDisposable接口的Transient服务实例则采用“即用即建,用后即弃”的策略。
我们接下来演示三种不同生命周期模式的差异。如下面代码片段所示,我们创建了一个ServiceCollection对象,并针对接口IFoo、IBar和IBaz注册了对应的服务,采用的生命周期模式分别为Transient、Scoped和Singleton。IServiceProvider对象被构建出来后,我们调用其CreateScope方法创建了两个代表“服务范围”的IServiceScope对象,它的ServiceProvider属性提供所在服务范围的IServiceProvider对象,实际上是当前IServiceProvider对象的子容器。我们最后利用作为子容器的这个IServiceProvider对象来提供所需的服务实例。
using App; using Microsoft.Extensions.DependencyInjection; var root = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar>(_ => new Bar()) .AddSingleton<IBaz, Baz>() .BuildServiceProvider(); var provider1 = root.CreateScope().ServiceProvider; var provider2 = root.CreateScope().ServiceProvider; GetServices<IFoo>(provider1); GetServices<IBar>(provider1); GetServices<IBaz>(provider1); Console.WriteLine(); GetServices<IFoo>(provider2); GetServices<IBar>(provider2); GetServices<IBaz>(provider2); static void GetServices<T>(IServiceProvider provider) { provider.GetService<T>(); provider.GetService<T>(); }
演示程序启动后会在控制台上输出如图1所示的结果。由于IFoo服务被注册为Transient服务,所以四次服务获取请求都会创建一个新的Foo对象。IBar服务的生命周期模式为Scoped,同一个IServiceProvider对象只会创建一个Bar对象,所以整个过程中会创建两个Bar对象。IBaz服务采用Singleton生命周期,具有同根的两个IServiceProvider对象提供的是同一个Baz对象。
图1 IServiceProvider对象按照服务注册对应的生命周期模式提供服务实例
[305]服务实例的释放回收
作为依赖注入容器的IServiceProvider对象不仅用来构建并提供服务实例,还负责管理这服务实例的生命周期。如果某个服务实例的类型实现了IDisposable接口,就意味着当生命周期完结的时候需要调用Dispose方法执行一些资源释放操作,针对服务实例的释放同样由IServiceProvider对象来负责。框架针对提供服务实例的释放策略取决于采用的生命周期模式,具体的策略如下。
- Transient和Scoped:所有实现了IDisposable接口的服务实例会被当前IServiceProvider对象保存起来,当IServiceProvider对象的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
- Singleton:服务实例保存在作为根容器的IServiceProvider对象上,只有当后者的Dispose方法被调用的时候,这些服务实例的Dispose方法才会随之被调用。
ASP.NET Core应用具有一个代表根容器的IServiceProvider对象,由于它与应用具有一致的生命周期而被称为ApplicationServices。对于处理的每一次请求,应用都会利用这个根容器来创建基于当前请求的服务范围,该服务范围所在的IServiceProvider对象被称为RequestServices,处理请求所需的服务实例均由它来提供。请求处理完成之后,创建的服务范围被终结,RequestServices也随之被释放,此时在当前请求范围内创建的Scoped服务实例和实现了IDisposable接口的Transient服务实例得以及时释放。
上述释放策略可以通过如下演示实例进行印证。如代码片段所示,我们并采用不同的生命周期模式添加了针对IFoo、IBar和IBaz的服务注册。在作为根容器的IServiceProvider对象被构建出来后,可以调用其CreateScope方法创建出对应的服务范围。我们利用服务范围所在的IServiceProvider对象提供了三个对应的实例。
using App; using Microsoft.Extensions.DependencyInjection; using (var root = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar, Bar>() .AddSingleton<IBaz, Baz>() .BuildServiceProvider()) { using (var scope = root.CreateScope()) { var provider = scope.ServiceProvider; provider.GetService<IFoo>(); provider.GetService<IBar>(); provider.GetService<IBaz>(); Console.WriteLine("Child container is disposed."); } Console.WriteLine("Root container is disposed."); }
由于代表根容器的IServiceProvider对象和服务范围的创建都是在using块中进行的,所以所有针对它们的Dispose方法都会在using块结束的地方被调用。该程序运行之后在控制台上输出的结果如图2所示,可以看到当作为子容器的IServiceProvider对象被释放的时候,由它提供的两个生命周期模式分别为Transient和Scoped的服务实例(Foo和Bar)被正常释放。对于生命周期模式为Singleton的服务实例Baz来说,它的Dispose方法会延迟到作为根容器的IServiceProvider对象被释放的时候才执行。
[306]服务范围的验证
Singleton和Scoped这两种不同的生命周期是通过将提供的服务实例分别存放到作为根容器的IServiceProvider对象和当前IServiceProvider对象来实现的,这意味着作为根容器的IServiceProvider对象提供的Scoped服务实例也是单例的。如果某个Singleton服务依赖另一个Scoped服务,那么Scoped服务实例将被一个Singleton服务实例所引用,也就意味着Scoped服务实例也成了一个Singleton服务实例。在ASP.NET Core应用中,我们一般只会将于请求具有一致生命周期的服务注册为Scope模式。一旦出现上述这种情况,就意味着Scoped服务实例将变成一个Singleton服务实例,这基本上不是我们希望看到的结果,这极有可能造成严重的内存泄露问题。为了避免这种情况的发生,框架提供了相应的验证机制。
如果希望IServiceProvider对象在提供服务时针对服务范围作有效性检验,我们只需要在调用IServiceCollection接口的BuildServiceProvider扩展方法时提供一个值为True作为参数即可。下面的演示程序定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar),其中,Foo需要依赖IBar。如果将IFoo和IBar分别注册为Singleton服务与Scoped服务,当调用BuildServiceProvider方法创建代表依赖注入容器的IServiceProvider对象的时候将validateScopes参数设置为True即可。下面这个实例演示了这种验证方式。
using App; using Microsoft.Extensions.DependencyInjection; var root = new ServiceCollection() .AddSingleton<IFoo, Foo>() .AddScoped<IBar, Bar>() .BuildServiceProvider(true); var child = root.CreateScope().ServiceProvider; ResolveService<IFoo>(root); ResolveService<IBar>(root); ResolveService<IFoo>(child); ResolveService<IBar>(child); void ResolveService<T>(IServiceProvider provider) { var isRootContainer = root == provider ? "Yes" : "No"; try { provider.GetService<T>(); Console.WriteLine($"Status: Success; Service Type: { typeof(T).Name}; Root: { isRootContainer}"); } catch (Exception ex) { Console.WriteLine($"Status: Fail; Service Type: { typeof(T).Name}; Root: { isRootContainer}"); Console.WriteLine($"Error: {ex.Message}"); } } public interface IFoo {} public interface IBar {} public class Foo : IFoo { public IBar Bar { get; } public Foo(IBar bar) => Bar = bar; } public class Bar : IBar {}
上面的演示实例启动之后在控制台上输出的结果如图3所示。从输出结果可以看出,四个服务提取请求只有一次(使用代表子容器的IServiceProvider提供IBar服务实例)是成功的。这个实例充分说明了一旦开启了针对服务范围的验证,IServiceProvider对象不可能提供以单例形式存在的Scoped服务实例。
[307]服务注册有效性的验证
针对服务范围的检验体现在ServiceProviderOptions配置选项的ValidateScopes属性上。如下面的代码片段所示,ServiceProviderOptions还具有另一个名为ValidateOnBuild的属性。如果将该属性设置为True,就意味着IServiceProvider对象被构建的时候会对每个ServiceDescriptor对象实施有效性验证。
public class ServiceProviderOptions { public bool ValidateScopes { get; set; } public bool ValidateOnBuild { get; set; } }
我们照例来做一个在构建IServiceProvider对象时检验服务注册有效性的例子。如下面的代码片段所示,我们定义了一个IFoobar接口和对应的实现类型Foobar。由于希望总是希望以单例的形式来使用Foobar对象,我们为了定义了唯一的私有构造函数。
public interface IFoobar {} public class Foobar : IFoobar { private Foobar() {} public static readonly Foobar Instance = new Foobar(); }
我们在演示程序中定义了如下这个BuildServiceProvider方法来完成针对IFoobar/Foobar的服务注册和最终对IServiceProvider对象的构建。我们在调用BuildServiceProvider扩展方法创建对应IServiceProvider对象时指定了一个ServiceProviderOptions对象,而该对象的ValidateOnBuild属性来源于内嵌方法的同名参数。
using App; using Microsoft.Extensions.DependencyInjection; BuildServiceProvider(false); BuildServiceProvider(true); static void BuildServiceProvider(bool validateOnBuild) { try { var options = new ServiceProviderOptions { ValidateOnBuild = validateOnBuild }; new ServiceCollection() .AddSingleton<IFoobar, Foobar>() .BuildServiceProvider(options); Console.WriteLine($"Status: Success; ValidateOnBuild: {validateOnBuild}"); } catch (Exception ex) { Console.WriteLine($"Status: Fail; ValidateOnBuild: {validateOnBuild}"); Console.WriteLine($"Error: {ex.Message}"); } }
由于Foobar具有唯一的私有构造函数,而提供的服务注册并不能将服务实例创建出来,所以这个服务注册是无效的。由于在默认情况下构建IServiceProvider对象的时候并不会对服务注册做有效性检验,所以此时无效的服务注册并不会及时被探测到。一旦将ValidateOnBuild选项设置为True,IServiceProvider对象在被构建的时候就会抛出异常,图4所示的输出结果就体现了这一点。