您现在的位置是:网站首页> 编程资料编程资料

理解ASP.NET Core 依赖注入(Dependency Injection)_实用技巧_

2023-05-24 342人已围观

简介 理解ASP.NET Core 依赖注入(Dependency Injection)_实用技巧_

依赖注入

什么是依赖注入

简单说,就是将对象的创建和销毁工作交给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"); } }
  • 在一个大型项目中,这样的代码分散在项目各处,涉及到的服务均需要进行修改,显然一个一个去修改不现实,且违反了“开闭原则”。
  • 如果Logger中还需要其他一些依赖项,那么用到Logger的服务也要为其提供依赖,如果依赖项修改了,其他服务也必须要进行更改,更加增大了维护难度。
  • 很难进行单元测试,因为它无法进行 mock

正因如此,所以依赖注入解决了这些棘手的问题:

  • 通过接口或基类(包含抽象方法或虚方法等)将依赖关系进行抽象化
  • 将依赖关系存放到服务容器中
  • 由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中

ASP.NET Core内置的依赖注入

服务生存周期

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(); // 超出范围(请求结束时)则释放 services.AddScoped(); // 程序停止时释放 services.AddSingleton(); // 程序停止时释放 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

TryAdd{Lifetime}扩展方法

当你将同样的服务注册了多次时,如:

 services.AddSingleton(); services.AddSingleton(); 

那么当使用IEnumerable<{Service}>(下面会讲到)解析服务时,就会产生多个MyService实例的副本。

为此,框架提供了TryAdd{Lifetime}扩展方法,位于命名空间Microsoft.Extensions.DependencyInjection.Extensions下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。

 services.AddTransient(); // 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关) services.TryAddTransient(); services.TryAddTransient();
  • TryAdd:通过参数ServiceDescriptor将服务类型、实现类型、生命周期等信息传入进去
  • TryAddTransient:对应AddTransient
  • TryAddScoped:对应AddScoped
  • TryAddSingleton:对应AddSingleton
  • TryAddEnumerable:这个和TryAdd的区别是,TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:
 // 注册了 IMyService - MyService1 services.TryAddEnumerable(ServiceDescriptor.Singleton()); // 注册了 IMyService - MyService2 services.TryAddEnumerable(ServiceDescriptor.Singleton()); // 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了 services.TryAddEnumerable(ServiceDescriptor.Singleton());

解析同一服务的多个不同实现

默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。

如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable<{Service}>来解析(顺序同注册顺序一致):

 public interface IAnimalService { } public class DogService : IAnimalService { } public class PigService : IAnimalService { } public class CatService : IAnimalService { } public void ConfigureServices(IServiceCollection services) { // 生命周期没有限制 services.AddTransient(); services.AddScoped(); services.AddSingleton(); } public ValuesController( // CatService IAnimalService animalService, // DogService、PigService、CatService IEnumerable animalServices) { }

Replace && Remove 扩展方法

上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用ReplaceRemove

 // 将 IMyService 的实现替换为 MyService1 services.Replace(ServiceDescriptor.Singleton()); // 移除 IMyService 注册的实现 MyService services.Remove(ServiceDescriptor.Singleton()); // 移除 IMyService 的所有注册 services.RemoveAll(); // 清除所有服务注册 services.Clear();

Autofac

Autofac 是一个老牌DI组件了,接下来我们使用Autofac替换ASP.NET Core自带的DI容器。

1.安装nuget包:

 Install-Package Autofac Install-Package Autofac.Extensions.DependencyInjection

2.替换服务提供器工厂

 public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) // 通过此处将默认服务提供器工厂替换为 autofac .UseServiceProviderFactory(new AutofacServiceProviderFactory());

3.在 Startup 类中添加 ConfigureContainer 方法

 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 
                
                

-六神源码网