NetCore 入门 (一) : 依赖注入
1. QuickStart
1.1 安装NuGet包
Microsoft.Extensions.DependencyInjection.Abstractions; // 抽象依赖包
Microsoft.Extensions.DependencyInjection; // 具体实现包
::: tip 包的设计原则
.Net Core提供的很多基础组件在设计时都会考虑“ 抽象和实现 ”的分离。例如要开发一款名为Foobar
的组件,微软会将一些接口和必要的类型定义在NuGet包FooBar.Abstractions
中,而将具体的实现类型定义在NuGet包FooBar
中。如果另一个组件或框架需要使用FooBar组件,或需要对FooBar组件进行扩展与定制,只需要添加包FooBar.Abstractions
即可。这种做法体现了“ 最小依赖 ”的设计原则。
:::
1.2 类型定义
public interface IFoo { }
public interface IBar { }
public interface IBaz { }
public class Foo : IFoo { }
public class Bar : IBar { }
public class Baz : IBaz { }
1.3 服务注册与消费
// 1. 创建服务集合
IServiceCollection services = new ServiceCollection();
// 2. 服务注册
// 有Singleton、Scoped、Transient三种生命周期
services.AddSingleton<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddTransient<IBaz, Baz>();
// 3. 构建ServiceProvider
var provider = services.BuildServiceProvider();
// 4、 服务消费并验证
Debug.Assert(provider.GetService<IFoo>() is Foo);
Debug.Assert(provider.GetService<IBar>() is Bar);
Debug.Assert(provider.GetService<IBaz>() is Baz);
2. 服务注册
这是依赖注入服务最核心的2个类:
- IServiceCollection:这是一个集合类型,负责收集服务的注册信息
- IServiceProvider:服务的提供者,通过该对象消费所需的服务
2.1 服务的注册方式
可以通过以下三种方式进行服务的注册:
- 指定服务的实现类型
- 指定服务的实现实例
- 指定服务的创建工厂
// 服务的实现类型
services.AddSingleton<IFoo, Foo>();
// 服务的实现实例
services.AddSingleton<IFoo>(new Foo());
// 服务的创建工厂
services.AddSingleton<IFoo>( _ => new Foo());
2.2 ServiceDescriptor
ServiceDescriptor
对象用来描述服务的注册信息。在前面的示例中,services
注册了不同生命周期的服务类型。本质上就是把服务注册的信息构建成了ServiceDescriptor
对象。
public class ServiceDescriptor
{
public Type ServiceType { get; }
public ServiceLifetime Lifetime { get; }
public Type ImplementationType { get; }
public object ImplementationInstance { get; }
public Func<IServiceProvider, object> ImplementationFactory { get; }
}
ServiceType
:服务的注册类型Lifetime
:服务的生命周期ImplementationType
:服务的实现类型ImplementationInstance
:服务的实现实例ImplementationFactory
: 服务的创建工厂
构造函数
可以通过构造函数来创建 ServiceDescriptor
对象。
public class ServiceDescriptor
{
// 未指定生命周期,默认采用Singleton
public ServiceDescriptor(Type serviceType, object instance);
public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime);
public ServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime);
}
Describe
方法
ServiceDescriptor
提供了2个名为Describe
的静态方法来创建ServiceDescriptor
对象。
public class ServiceDescriptor
{
public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory,
ServiceLifetime lifetime);
public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime);
}
简化方法
以上两种方式都需要指定生命周期。为了简化对象的创建方式,ServiceDescriptor
定义了一系列针对生命周期的静态生命周期。下面展示针对Singleton
的方法。针对Scoped
和Transient
也具有类似的定义。
public class ServiceDescriptor
{
public static ServiceDescriptor Singleton(Type serviceType, object implementationInstance);
public static ServiceDescriptor Singleton(Type service, Type implementationType);
public static ServiceDescriptor Singleton(Type serviceType, Func<IServiceProvider, object> implementationFactory);
public static ServiceDescriptor Singleton<TService>(TService implementationInstance);
public static ServiceDescriptor Singleton<TService, TImplementation>();
public static ServiceDescriptor Singleton<TService>(Func<IServiceProvider, TService> implementationFactory);
public static ServiceDescriptor Singleton<TService, TImplementation>(
Func<IServiceProvider, TImplementation> implementationFactory
);
}
2.3 IServiceCollection
IServiceCollection
对象本质上是ServiceDescriptor
的列表,针对服务的注册就是创建ServiceDescriptor
对象并添加到IServiceCollection
集合中的过程。
// IServiceCollection 接口
public interface IServiceCollection : IList<ServiceDescriptor> { }
public class ServiceCollection : IServiceCollection { }
Add
考虑到服务注册是一个高频操作,IServiceCollection
接口提供了一系列扩展方法。如下2个Add
方法将指定的一个或多个ServiceDescriptor
添加到集合中。
public static class ServiceCollectionDescriptorExtensions
{
public static IServiceCollection Add(this IServiceCollection collection,ServiceDescriptor descriptor);
public static IServiceCollection Add(this IServiceCollection collection,IEnumerable<ServiceDescriptor> descriptors);
}
同时提供了针对生命周期的注册方法:
public static class ServiceCollectionServiceExtensions
{
public static IServiceCollection AddScoped<TService>(this IServiceCollection collection);
...
public static IServiceCollection AddSingleton<TService>(this IServiceCollection collection);
...
public static IServiceCollection AddTransient<TService>(this IServiceCollection collection);
...
}
Add
类方法允许针对同一个服务类型添加多个ServiceDescriptor
对象。在调用GetService<T>
获取服务时,依赖注入容器会根据最近添加的ServiceDescriptor
来创建服务实例。也就是说,多次进行服务注册,后者会把前者覆盖掉。
TryAdd
在大多数情况下,同一个服务类型存在一个ServiceDescriptor
对象就够了。TryAdd
方法会在注册前,根据服务的注册类型判断对应的ServiceDescriptor
是否存在,即服务的存在性检验。只有在不存在的情况下,才会进行服务注册。
public static class ServiceCollectionDescriptorExtensions
{
public static void TryAdd(this IServiceCollection collection, ServiceDescriptor descriptor);
public static void TryAdd(this IServiceCollection collection, IEnumerable<ServiceDescriptor> descriptors);
public static void TryAddSingleton<TService, TImplementation>(this IServiceCollection collection);
...
public static void TryAddScoped<TService, TImplementation>(this IServiceCollection collection);
...
public static void TryAddTransient<TService, TImplementation>(this IServiceCollection collection);
...
}
TryAddEnumerable
IServiceCollection
接口还具有如下两个名为TryAddEnumerable的方法。与TryAdd
方法一样,TryAddEnumerable
也会做ServiceDescriptor
的存在性检验。
public static class ServiceCollectionDescriptorExtensions
{
public static void TryAddEnumerable(this IServiceCollection collection, ServiceDescriptor descriptor);
public static void TryAddEnumerable(this IServiceCollection collection, IEnumerable<ServiceDescriptor> descriptors);
}
二者的区别在于:
TryAdd
只根据 注册类型 判断服务注册是否存在。TryAddEnumerable
要同时根据 注册类型 和 实现类型 来判断服务注册是否存在。
也就是说,对于每个注册类型,使用TryAdd
,集合中只允许存在一个实现类型;而使用TryAddEnumerable
,允许存在多个不同的实现类型。
Factory 和 Lambda
::: danger
创建工厂的返回类型(即服务的实现类型),将被用于判断ServiceDescriptor
的存在性。如果返回类型是object
,会因为实现类型不明确而抛出异常ArgumentException
。
:::
// 实例工厂
Func<IServiceProvider, Foo> factory= _ => new Foo(); // 返回类型为Foo
services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo>(factory));
// lambda表达式
services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo>( _ => new Foo())); // 返回类型为object,抛出异常ArgumentException
对于lambda表达式的实例工厂,本质是一个Func<IServiceProvider, object>
对象,此时会因为实现类型不明确而抛出异常ArgumentException
。
3. 服务消费
3.1 IServiceProvider
IServiceProvider
接口作为服务提供者,定义了唯一的GetService
方法,可以根据指定的类型来提供对应的服务实例。
public interface IServiceProvider
{
object GetService(Type serviceType);
}
IServiceProvider
还提供了如下几个扩展方法:
public static class ServiceProviderServiceExtensions{
public static T GetService<T>(this IServiceProvider provider);
public static object GetRequiredService(this IServiceProvider provider, Type serviceType);
public static T GetRequiredService<T>(this IServiceProvider provider);
public static IEnumerable<T> GetServices<T>(this IServiceProvider provider);
public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType);
}
- GetRequiredService
如果指定服务类型的服务注册不存在,GetService
方法会返回null,而GetRequiredService
方法会抛出InvalidOperationException
异常。
- GetServices
如果希望获取某一服务的所有实现类型,可以调用GetServices<T>
方法,也可以这样调用:GetService<IEnumerable<T>>
。
3.2 构造函数选择原则
依赖注入服务提供了三种服务的注册方式:分别是指定服务的实现类型
、指定服务的实现实例
、指定服务的创建工厂
。对于后两者,服务的实现实例很容易获得。对于第一种注册方式,
则需要调用该类型的构造函数。那么在存在多个构造函数的情况下,该如何创建服务的实现实例呢?
::: tip 原则1
候选构造函数具备一个基本的条件:IServiceProvider
对象能够提供构造函数的所有参数。
:::
::: tip 原则2
每个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。
:::
::: danger
如果无法通过以上2个原则确定最终的构造函数,系统将抛出异常InvalidOperationException
。
:::
示例演示
// 定义服务接口
public interface IFoo { }
public interface IBar { }
public interface IBaz { }
public interface IQux { }
// 定义服务实现类型
public class Foo : IFoo { }
public class Bar : IBar { }
public class Baz : IBaz { }
public class Qux : IQux
{
public Qux(IFoo foo) => Console.WriteLine("Selected ctor: Qux(IFoo)");
public Qux(IFoo foo, IBar bar) => Console.WriteLine("Selected ctor: Qux(IFoo, IBar)");
public Qux(IFoo foo, IBar bar, IBaz baz) => Console.WriteLine("Selected ctor: Qux(IFoo, IBar, IBaz)");
}
class Program
{
static void Main(string[] args)
{
// 1. 创建服务集合
IServiceCollection services = new ServiceCollection();
// 2. 服务注册
services.AddTransient<IFoo, Foo>();
services.AddTransient<IBar, Bar>();
services.AddTransient<IQux, Qux>();
// 3. 构建ServiceProvider
var provider = services.BuildServiceProvider();
// 4. 获取服务实例
provider.GetService<IQux>();
}
}
对于定义在Qux
中的3个构造函数来说,由于IServiceCollection
集合包含IFoo
接口和IBar
接口的服务注册,所以能够提供前面2个构造函数的所有参数。第三个构造函数具有一个IBaz
类型的参数,无法通过IServiceProvider
对象来提供。
根据原则1, Qux
的前两个构造函数成为合法的候选构造函数。
根据原则2, Qux
的第二个构造函数的参数类型包含IFoo
和IBar
,而第一个构造函数只具有一个类型为IFoo
的参数,所以最终选择的是第二个构造函数。
运行实例程序,输出结果如下所示:
Selected ctor: Qux(IFoo, IBar)
4. 生命周期
依赖注入框架(DI)支持三种生命周期模式:
public enum ServiceLifetime
{
// Specifies that a single instance of the service will be created.
Singleton = 0,
// Specifies that a new instance of the service will be created for each scope.
// In ASP.NET Core applications a scope is created around each server request.
Scoped = 1,
// Specifies that a new instance of the service will be created every time it
// is requested.
Transient = 2
}
Singleton
: 单例模式,在应用程序的整个生命周期中仅存在唯一的一个实例。Scoped
: 作用域模式,在Scoped
范围内存在唯一的一个实例。Transient
: 瞬态模式,在每次请求时,都将返回一个新的实例。
4.1 理解Scoped
Singleton
和Transient
模式具有明确的语义,我们方便理解,但Scoped
的生命周期就不是很好懂了。
看下Scoped
的注解:
In ASP.NET Core applications a scope is created around each server request.
在ASP.NET Core应用程序中,服务器的每次请求都会创建一个scope对象。也就是说,Scoped
作用域范围就是服务器的一次请求。服务器开始处理请求,一个Scoped
对象被创建;请求处理结束,Scoped
作用域结束,Scoped
对象被销毁。
Scoped
对象是由IServiceScope
接口表示的服务范围, 该范围是由IServiceScopeFactory
表示的服务范围工厂来创建的。
public interface IServiceScope : IDisposable
{
IServiceProvider ServiceProvider { get; }
}
public interface IServiceScopeFactory
{
IServiceScope CreateScope();
}
public static class ServiceProviderServiceExtensions
{
public static IServiceScope CreateScope(this IServiceProvider provider)
=> provider.GetService<IServiceScopeFactory>().CreateScope();
}
任何一个IServiceProvider
对象都可以利用其扩展方法CreateScope
创建IServiceScope
对象。从上面的定义可以看出,每个IServiceScope
对象都包含一个IServiceProvider
属性,该属性与创建它的IServiceProvider
对象在逻辑上保持如下图所示的“父子关系”。
但是从功能实现层面来说,DI并不需要维护这样的“父子关系”。IServiceProvider
对象不需要知道自己的“父亲”是谁,它只关心作为根节点的IServiceProvider
对象在哪里。下图揭示了IServiceScope
和IServiceProvider
之间的引用关系。任何一个IServiceProvider
对象都具有针对根容器的引用。
Scoped使用示例
// 类型定义
public interface IFoo { }
public class S103_Foo : IFoo { }
class Program
{
static void Main(string[] args)
{
// 1. 创建服务集合
IServiceCollection services = new ServiceCollection();
// 2. 服务注册
services.AddScoped<IFoo, S103_Foo>();
// 3. 构建依赖注入容器
var provider = services.BuildServiceProvider();
// 4. CreateScope
using (var scope = provider.CreateScope())
{
var foo = scope.ServiceProvider.GetService<IFoo>();
};
}
}
4.2 服务的提供
针对不同的生命周期,DI采用不同的策略来提供服务实例。
Singleton
: 在第一次请求时创建该服务实例,并保存在作为根节点的IServiceProvider
对象中。这样保证了多个同根的IServiceProvider
对象提供的Singleton
实例都是同一个对象。Scoped
:IServiceProvider
对象创建的实例由自己保存。Transient
: 针对每次服务提供请求,IServiceProvider
对象总是返回一个新的服务实例。
4.3 服务的释放
IServiceProvider
还具有回收释放的作用。这里的回收释放与.Net Core自身的垃圾回收机制无关,仅仅针对实现了IDispose
或IDisposeAsync
接口的服务实例(下面称Disposable服务实例)。通过调用Dispose
或DisposeAsync
方法,来完成服务实例的回收释放。
Singleton
: Disposable服务实例被保存在作为根节点的IServiceProvider
对象上,只有这个根节点IServiceProvider
对象被释放的时候,这些Disposable服务实例才会被释放。Scoped
和Transient
:Disposable服务实例被保存在创建它的IServiceProvider
对象上,当这个对象释放的时候,这些Disposable服务实例就会被释放。
4.4 服务验证
如果某个Singleton
服务依赖于一个Scoped
服务,那么只有当Singleton
服务实例释放的时候,被依赖的Scoped
服务实例才能被释放。在这种情况下,Scoped
服务实例变成了一个Singleton
服务实例。
在ASP.NET Core应用中,将某个服务的生命周期设置为Scoped
是希望依赖注入容器根据接收的每个请求来创建和释放服务实例。如果出现上述这种情况,Scoped
服务实例引用的资源(数据库连接,IO等)就得不到及时释放,这无疑不是我们希望看到的结果。
在调用IServiceCollection
接口的BuilderServiceProvider
方法时,可以开启服务验证。
public static class ServiceCollectionContainerBuilderExtensions
{
// 参数:
// validateScopes:
// true to perform check verifying that scoped services never gets resolved from
// root provider; otherwise false.
//
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes);
// 参数:
// options:
// Configures various service provider behaviors.
//
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options);
}
public class ServiceProviderOptions
{
//
// 摘要:
// true to perform check verifying that scoped services never gets resolved from
// root provider; otherwise false. Defaults to false.
public bool ValidateScopes { get; set; }
//
// 摘要:
// true to perform check verifying that all services can be created during
// BuildServiceProvider call; otherwise false. Defaults to false. NOTE: this check doesn't verify open
// generics services.
public bool ValidateOnBuild { get; set; }
}
ValidateScopes
ValidateScopes
参数保证IServiceProvider
对象不可能以单例形式提供Scoped
服务。
示例:
public interface IFoo { }
public interface IBar { }
public class Foo : IFoo { }
public class Bar : IBar
{
public Bar(IFoo foo) => Console.WriteLine("Bar Created");
}
class Program
{
static void Main(string[] args)
{
// 1. 创建服务集合
IServiceCollection services = new ServiceCollection();
// 2. 服务注册
services.AddScoped<IFoo, Foo>();
services.AddSingleton<IBar, Bar>();
// 3. 构建ServiceProvider
var provider = services.BuildServiceProvider(true);
try
{
var bar = provider.GetService<IBar>();
}
catch (Exception ex)
{
Console.WriteLine($"Error:{ex.Message}");
}
}
}
结果输出
Error:Cannot consume scoped service 'DependencyInjection.IFoo'
from singleton 'DependencyInjection.IBar'.
ValidateOnBuild
如果ValidateOnBuild
参数设置为true, 则IServiceProvider
对象被构建的时候会检查每个注册服务是否都可以被正常创建。
public interface IBar { }
public class Bar : IBar
{
private Bar() => Console.WriteLine("Bar Created");
}
class Program
{
static void Main(string[] args)
{
// 1. 创建服务集合
IServiceCollection services = new ServiceCollection();
// 2. 服务注册
services.AddSingleton<IBar, Bar>();
ServiceProviderOptions options = new ServiceProviderOptions
{
ValidateOnBuild = true
};
try
{
// 3. 构建ServiceProvider
var provider = services.BuildServiceProvider(options);
}
catch (Exception ex)
{
Console.WriteLine($"Error:{ex.Message}");
}
}
}
结果输出
Error:Some services are not able to be constructed (Error while validating the service descriptor
'ServiceType: DependencyInjection.IBar Lifetime: Singleton ImplementationType: DependencyInjection.Bar':
A suitable constructor for type 'DependencyInjection.Bar' could not be located.
Ensure the type is concrete and services are registered for all parameters of a public constructor.)
5. 整合第三方框架
目前市场上还有很多开源的依赖注入框架,比较常用的有Castle、StructureMap、Spring.NET、AutoFac、Unity和Ninject等。如果开发者习惯使用这些第三方框架,可以借助DependencyInjection的整合方式。
DependencyInjection定义了使用依赖注入的基本方式:服务注册信息存储在IServiceCollection
集合中,消费者通过IServiceProvider
对象获得服务实例。如果要将第三方框架整合进来,就必须解决从IServiceCollection
集合到IServiceProvider
对象的适配问题。
5.1 IServiceProviderFactory
public interface IServiceProviderFactory<TContainerBuilder>
{
TContainerBuilder CreateBuilder(IServiceCollection services);
IServiceProvider CreateServiceProvider(TContainerBuilder containerBuilder);
}
借助IServiceProviderFactory
接口,将第三方框架整合进来:
- CreateBuilder:利用
IServiceCollection
集合创建针对某个框架的ContainerBuilder
对象。 - CreateServiceProvider:
ContainerBuilder
对象提供创建IServiceProvider
对象的方法。
5.2 DefaultServiceProviderFactory
这个是DependencyInjection框架内部的默认实现方式。
public class DefaultServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
private readonly ServiceProviderOptions _options;
public DefaultServiceProviderFactory() : this(ServiceProviderOptions.Default){}
public DefaultServiceProviderFactory(ServiceProviderOptions options)
{
this._options = options;
}
public IServiceCollection CreateBuilder(IServiceCollection services)
{
return services;
}
public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
{
return containerBuilder.BuildServiceProvider(this._options);
}
}
5.3 示例
// step 1: 提供 ServiceCollection
var services = new ServiceCollection()
.AddSingleton<IFoo, Foo>()
.AddSingleton<IBar, Bar>();
// step 2: 创建并配置TContainerBuilder
var factory = XxxServiceProviderFactory();
var builder = factory.CreateBuilder(services);
builder.optionXxx(...)
.optionXxx(...)
.optionXxx(...);
// step 3: 获得 IServiceProvider
IServiceProvider serviceProvider = factory.CreateServiceProvider(builder);