注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录
简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。
依赖注入在.NET中,可谓是“一等公民”,处处都离不开它,那么它有什么好处呢?
假设有一个日志类 FileLogger,用于将日志记录到本地文件。
public class FileLogger { public void LogInfo(string message) { } }
日志很常用,几乎所有服务都需要记录日志。如果不使用依赖注入,那么我们就必须在每个服务中手动 new FileLogger 来创建一个 FileLogger 实例。
public class MyService { private readonly FileLogger _logger = new FileLogger(); public void Get() { _logger.LogInfo("MyService.Get"); } }
如果某一天,想要替换掉 FileLogger,而是使用 ElkLogger,通过ELK来处理日志,那么我们就需要将所有服务中的代码都要改成 new ElkLogger。
public class MyService { private readonly ElkLogger _logger = new ElkLogger(); public void Get() { _logger.LogInfo("MyService.Get"); } }
正因如此,所以依赖注入解决了这些棘手的问题:
Transient
瞬时,即每次获取,都是一个全新的服务实例
Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。
Singleton
单例,即在单个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。
若服务实现了IDisposable
接口,并且该服务是由DI容器创建的,那么你不应该去Dispose
,DI容器会对服务自动进行释放。
如,有Service1、Service2、Service3、Service4四个服务,并且都实现了IDisposable
接口,如:
public class Service1 : IDisposable { public void Dispose() { Console.WriteLine("Service1.Dispose"); } } public class Service2 : IDisposable { public void Dispose() { Console.WriteLine("Service2.Dispose"); } } public class Service3 : IDisposable { public void Dispose() { Console.WriteLine("Service3.Dispose"); } } public class Service4 : IDisposable { public void Dispose() { Console.WriteLine("Service4.Dispose"); } }
并注册为:
public void ConfigureServices(IServiceCollection services) { // 每次使用完(请求结束时)即释放 services.AddTransient<Service1>(); // 超出范围(请求结束时)则释放 services.AddScoped<Service2>(); // 程序停止时释放 services.AddSingleton<Service3>(); // 程序停止时释放 services.AddSingleton(sp => new Service4()); }
构造函数注入一下
public ValuesController( Service1 service1, Service2 service2, Service3 service3, Service4 service4) { }
请求一下,获取输出:
Service2.Dispose Service1.Dispose
这些服务实例都是由DI容器创建的,所以DI容器也会负责服务实例的释放和销毁。注意,单例此时还没到释放的时候。
但如果注册为:
public void ConfigureServices(IServiceCollection services) { // 注意与上面的区别,这个是直接 new 的,而上面是通过 sp => new 的 services.AddSingleton(new Service1()); services.AddSingleton(new Service2()); services.AddSingleton(new Service3()); services.AddSingleton(new Service4()); }
此时,实例都是咱们自己创建的,DI容器就不会负责去释放和销毁了,这些工作都需要我们开发人员自己去做。
更多注册方式,请参考官方文档-Service registration methods
当你将同样的服务注册了多次时,如:
services.AddSingleton<IMyService, MyService>(); services.AddSingleton<IMyService, MyService>();
那么当使用IEnumerable<{Service}>
(下面会讲到)解析服务时,就会产生多个MyService
实例的副本。
为此,框架提供了TryAdd{Lifetime}
扩展方法,位于命名空间Microsoft.Extensions.DependencyInjection.Extensions
下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。
services.AddTransient<IMyService, MyService1>(); // 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关) services.TryAddTransient<IMyService, MyService1>(); services.TryAddTransient<IMyService, MyService2>();
ServiceDescriptor
将服务类型、实现类型、生命周期等信息传入进去TryAdd
的区别是,TryAdd
仅根据服务类型来判断是否要进行注册,而TryAddEnumerable
则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:// 注册了 IMyService - MyService1 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>()); // 注册了 IMyService - MyService2 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>()); // 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了 services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。
如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable<{Service}>
来解析(顺序同注册顺序一致):
public interface IAnimalService { } public class DogService : IAnimalService { } public class PigService : IAnimalService { } public class CatService : IAnimalService { } public void ConfigureServices(IServiceCollection services) { // 生命周期没有限制 services.AddTransient<IAnimalService, DogService>(); services.AddScoped<IAnimalService, PigService>(); services.AddSingleton<IAnimalService, CatService>(); } public ValuesController( // CatService IAnimalService animalService, // DogService、PigService、CatService IEnumerable<IAnimalService> animalServices) { }
上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用Replace
和Remove
了
// 将 IMyService 的实现替换为 MyService1 services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>()); // 移除 IMyService 注册的实现 MyService services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>()); // 移除 IMyService 的所有注册 services.RemoveAll<IMyService>(); // 清除所有服务注册 services.Clear();
Autofac 是一个老牌DI组件了,接下来我们使用Autofac替换ASP.NET Core自带的DI容器。
Install-Package Autofac Install-Package Autofac.Extensions.DependencyInjection
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) // 通过此处将默认服务提供器工厂替换为 autofac .UseServiceProviderFactory(new AutofacServiceProviderFactory());
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public ILifetimeScope AutofacContainer { get; private set; } public void ConfigureServices(IServiceCollection services) { // 1. 不要 build 或返回任何 IServiceProvider,否则会导致 ConfigureContainer 方法不被调用。 // 2. 不要创建 ContainerBuilder,也不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了 // 3. 你仍然可以在此处通过微软默认的方式进行服务注册 services.AddOptions(); services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" }); }); } // 1. ConfigureContainer 用于使用 Autofac 进行服务注册 // 2. 该方法在 ConfigureServices 之后运行,所以这里的注册会覆盖之前的注册 // 3. 不要 build 容器,不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了 public void ConfigureContainer(ContainerBuilder builder) { // 将服务注册划分为模块,进行注册 builder.RegisterModule(new AutofacModule()); } public class AutofacModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { // 在此处进行服务注册 builder.RegisterType<UserService>().As<IUserService>(); } } public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { // 通过此方法获取 autofac 的 DI容器 AutofacContainer = app.ApplicationServices.GetAutofacRoot(); } }
上面我们主要讲了服务的注入方式,接下来看看服务的解析方式。解析方式有两种:
上面我们举得很多例子都是使用了构造函数注入——通过构造函数接收参数。构造函数注入是非常常见的服务注入方式,也是首选方式,这要求:
IServiceProvider
解析时,要求构造函数必须是publicActivatorUtilities
解析时,要求构造函数必须是public,虽然支持构造函数重载,但必须只能有一个是有效的,即参数能够全部通过依赖注入得到值顾名思义,方法注入就是通过方法参数来接收服务实例。
[HttpGet] public string Get([FromServices]IMyService myService) { return "Ok"; }
ASP.NET Core内置的依赖注入是不支持属性注入的。但是Autofac支持,用法如下:
老规矩,先定义服务和实现
public interface IUserService { string Get(); } public class UserService : IUserService { public string Get() { return "User"; } }
然后注册服务
AddControllersAsServices
方法,将控制器交给 autofac 容器来处理,这样就可以使“属性注入”生效了public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddControllersAsServices(); } public void ConfigureContainer(ContainerBuilder builder) { builder.RegisterModule<AutofacModule>(); } public class AutofacModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<UserService>().As<IUserService>(); var controllerTypes = Assembly.GetExecutingAssembly().GetExportedTypes() .Where(type => typeof(ControllerBase).IsAssignableFrom(type)) .ToArray(); // 配置所有控制器均支持属性注入 builder.RegisterTypes(controllerTypes).PropertiesAutowired(); } }
最后,我们在控制器中通过属性来接收服务实例
public class ValuesController : ControllerBase { public IUserService UserService { get; set; } [HttpGet] public string Get() { return UserService.Get(); } }
通过调用Get
接口,我们就可以得到IUserService
的实例,从而得到响应
User
GetService
来获取服务实例,而应该使用DI。using Microsoft.Extensions.DependencyInjection; public class ValuesController : ControllerBase { private readonly IServiceProvider _serviceProvider; // 应通过依赖注入的方式获取服务实例 public ValuesController(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [HttpGet] public string Get() { // 尽量避免通过 GetService 方法获取服务实例 var myService = _serviceProvider.GetService<IMyService>(); return "Ok"; } }
ConfigureServices
中调用BuildServiceProvider
。因为这会导致创建第二个DI容器的副本,从而导致注册的单例服务出现多个副本。public void ConfigureServices(IServiceCollection services) { // 不要在该方法中调用该方法 var serviceProvider = services.BuildServiceProvider(); }
一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:
当在Development
环境中运行、并通过 CreateDefaultBuilder
生成主机时,默认的服务提供程序会进行如下检查:
随着业务增长,需要依赖注入的服务也越来越多,建议使用扩展方法,封装服务注入,命名为Add{Group_Name}
,如将所有 AppService 的服务注册封装起来
namespace Microsoft.Extensions.DependencyInjection { public static class ApplicationServiceCollectionExtensions { public static IServiceCollection AddApplicationService(this IServiceCollection services) { services.AddTransient<Service1>(); services.AddScoped<Service2>(); services.AddSingleton<Service3>(); services.AddSingleton(sp => new Service4()); return services; } } }
然后在ConfigureServices
中调用即可
public void ConfigureServices(IServiceCollection services) { services.AddApplicationService(); }
以下列出一些常用的框架已经默认注册的服务:
服务类型 | 生命周期 |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Transient |
IHostApplicationLifetime | Singleton |
IHostLifetime | Singleton |
IWebHostEnvironment | Singleton |
IHostEnvironment | Singleton |
Microsoft.AspNetCore.Hosting.IStartup | Singleton |
Microsoft.AspNetCore.Hosting.IStartupFilter | Transient |
Microsoft.AspNetCore.Hosting.Server.IServer | Singleton |
Microsoft.AspNetCore.Http.IHttpContextFactory | Transient |
Microsoft.Extensions.Logging.ILogger |
Singleton |
Microsoft.Extensions.Logging.ILoggerFactory | Singleton |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Singleton |
Microsoft.Extensions.Options.IConfigureOptions |
Transient |
Microsoft.Extensions.Options.IOptions |
Singleton |
System.Diagnostics.DiagnosticSource | Singleton |
System.Diagnostics.DiagnosticListener | Singleton |