ASP.NET Core 依赖注入(构造函数注入,属性注入等)

原文: ASP.NET Core 依赖注入(构造函数注入,属性注入等)

若是你不熟悉ASP.NET Core依赖注入,先阅读文章: 在ASP.NET Core中使用依赖注入html

 

构造函数注入

构造函数注入经常使用于在服务构建上定义和获取服务依赖。例如:git

public class ProductService
  {
      private readonly IProductRepository _productRepository;
      public ProductService(IProductRepository productRepository)
      {
          _productRepository = productRepository;
      }
      public void Delete(int id)
      {
         _productRepository.Delete(id);
     }
 }

ProductService 将 IProductRepository做为依赖注入到它的构造函数,而后在 Delete 方法内部使用这个依赖。github

实践指南:

  • 在服务构造函数中明确地定义必需的依赖。所以该服务在没有这些依赖时没法被构造。
  • 将注入的依赖赋值给只读(readonly)的字段或属性(为了防止在内部方法中意外地赋予其余值)。

属性注入

ASP.NET Core 的标准依赖注入容器不支持属性注入。可是你可使用其余容器支持属性注入。例如:缓存

 

using Microsoft.Extensions.Logging;
  using Microsoft.Extensions.Logging.Abstractions;
  namespace MyApp
  {
      public class ProductService
      {
          public ILogger<ProductService> Logger { get; set; }           private readonly IProductRepository _productRepository;
          public ProductService(IProductRepository productRepository)
         {
             _productRepository = productRepository;
             Logger = NullLogger<ProductService>.Instance;          }
         public void Delete(int id)
         {
             _productRepository.Delete(id);
  Logger.LogInformation( $"Deleted a product with id = {id}");          }
     }
 }

ProductService 定义了一个带公开setter的Logger 属性。安全

依赖注入容器能够设置 Logger属性,若是它可用(已经注册到DI容器)。多线程

实践指南:

  • 仅对可选依赖使用属性注入。这意味着你的服务能够在没有提供这些依赖时正常地工做。
  • 若是可能,使用空对象模式(就像这个例子中这样)。不然,在使用这个依赖时始终检查是否为null

服务定位器

服务定位器模式是获取依赖关系的另一种方式。例如:并发

 

public class ProductService
  {
      private readonly IProductRepository _productRepository;
      private readonly ILogger<ProductService> _logger;
      public ProductService(IServiceProvider serviceProvider)
      {
          _productRepository = serviceProvider
            .GetRequiredService<IProductRepository>();
          _logger = serviceProvider
           .GetService<ILogger<ProductService>>() ??
             NullLogger<ProductService>.Instance;
     }
     public void Delete(int id)
     {
         _productRepository.Delete(id);
         _logger.LogInformation($"Deleted a product with id = {id}");
     }
 }

ProductService 注入了 IServiceProvider 来解析并使用依赖。 若是请求的依赖以前没有被注册,那么GetRequiredService将会抛出异常。换句话说, 这种状况下,GetService只会返回null。框架

当你在构造函数内部解析服务时,它们会随着服务的释放而释放。所以,你没必要关心构造函数内部已解析服务的释放问题(就像构造函数注入和属性注入)。ide

实践指南

  • 尽量不要使用服务定位模式(除非服务类型在开发时就已经知道)。由于它让依赖不明确。这意味着在建立服务实例期间不可能容易地看出依赖关系。这对单元测试来讲尤其重要,由于你可能想要模拟一些依赖。
  • 若是可能,在服务构造函数中解析依赖。在服务方法中解析会使你的程序更加难懂、更加容易出错。我将在下一个章节讨论问题和解决方案。

服务生命周期

 下面是服务在ASP.NET Core依赖注入中的生命周期:函数

  1. Transient 类型的服务在每次注入或请求的时候被建立。
  2. Scoped 类型的服务按照做用域被建立。在Web程序中,每一个Web请求都会建立新的隔离的服务做用域。这意味着Scoped类型的服务一般会根据Web请求建立。
  3. Singleton 类型的服务由DI容器建立。这一般意味着它们根据应用程序仅仅被建立一次,而后用于应用程序的整个生命周期。

 

DI容器会持续跟踪全部已经被解析的服务。当服务的生命周期终止时,它们会被释放并销毁:

  • 若是服务还有依赖,它们一样会被自动释放并销毁。
  • 若是服务实现了 IDisposable 接口,Dispose 方法会在服务释放时自动被调用。  

实践指南:

  • 尽量地将你的服务注册为 Transient 类型。由于设计Transient服务是简单的。你一般不用关心多线程问题内存泄漏问题,而且你知道这类服务只有很短的生存期。
  • 谨慎使用 Scoped 类型服务生命周期,由于若是你建立了子服务做用域或者由非Web程序使用这些服务,那么它会变得诡异复杂。
  • 谨慎使用Singleton 类型的生命周期,由于你须要处理多线程问题和潜在的内存泄漏问题
  • 不要在Singleton服务上依赖 Transient类型或者 Scoped类型的服务。由于当单例服务注入的时候,Transient服务也会变成单例实例。而且若是Transient服务不是设计用于支持这样的场景的话则可能会致使一些问题。ASP.NET Core的默认DI容器在这种状况下会抛出异常

在方法体中解析服务

在某些状况下,你可能须要在你的服务的某个方法中解析另外一个服务。 这种状况下,请确保在使用后释放该服务。保障这个的最好方法是建立一个服务做用域。例如:

public class PriceCalculator
  {
      private readonly IServiceProvider _serviceProvider;
      public PriceCalculator(IServiceProvider serviceProvider)
      {
          _serviceProvider = serviceProvider;
      }
      public float Calculate(Product product, int count,
        Type taxStrategyServiceType)
     {
         using (var scope = _serviceProvider.CreateScope())
         {
             var taxStrategy = (ITaxStrategy)scope.ServiceProvider                .GetRequiredService(taxStrategyServiceType);
             var price = product.Price * count;
             return price + taxStrategy.CalculateTax(price);
         }
     }
 }

PriceCalculator 在构造函数中注入了 IServiceProvider,并赋值给了一个字段。而后,PriceCalculator使用它在 Calculate方法内部建立了一个子服务做用域。该做用域使用 scope.ServiceProvider来解析服务,替代了注入的 _serviceProvider 实例。所以,在using语句结束后,全部从该做用域解析的服务都会自动释放并销毁。

实践指南:

  • 若是你在某个方法体内解析服务,始终建立一个子服务做用域来确保解析出的服务被正确地释放。
  • 若是某个方法使用 IServiceProvider做为参数,你能够直接从它解析服务,而且没必要关心服务的释放和销毁。建立和管理服务做用域是调用你方法的代码的职责。遵循这个原则可使你的代码更加整洁。
  • 不要让解析到的服务持有引用!不然,它可能致使内存泄漏。而且当你后面在使用对象引用时,你可能访问到一个已经销毁的服务。(除非解析到的服务是单例)

Singleton服务

单例服务一般设计用于保持应用程序状态。缓存是一个应用程序状态的好例子。例如:

public class FileService
  {
      private readonly ConcurrentDictionary<string, byte[]> _cache;
      public FileService()
      {
          _cache = new ConcurrentDictionary<string, byte[]>();       }
      public byte[] GetFileContent(string filePath)
      {
         return _cache.GetOrAdd(filePath, _ =>
         {
             return File.ReadAllBytes(filePath);
         });
     }
 }

FileService简单地缓存了文件内容以减小磁盘读取。这个服务应该被注册为一个单例,不然,缓存将没法按照预期工做。

实践指南:

  • 若是服务持有状态,那它应该以线程安全的方式来访问这个状态。由于全部请求会并发地使用该服务的同一个实例。我使用 ConcurrentDictionary 替代 Dictionary 来确保线程安全。
  • 不要在单例服务中使用Transient或Scoped服务。由于Transient服务可能不是设计为线程安全的。若是你使用了它们,在使用这些服务期间须要处理多线程问题(对实例使用lock语句)
  • 内存泄漏一般由单例服务致使。在应用程序结束前单例服务不会被释放/销毁。所以,若是这些单例服务实例化了类(或注入)可是没有释放/销毁,这些类会一直保留在内存中,直到应用程序结束。确保适时地释放/销毁这些类。见上面“在方法体中解析服务”的章节。
  • 若是你缓存数据(本例中的文件内容),当原始数据源发生变化时,你应该建立一个机制来更新/失效缓存的数据。

Scoped 服务

Scoped 生命周期的服务看起来是一个不错的存储每一个Web请求数据的好方法。由于ASP.NET Core为每一个Web请求建立一个服务做用域。所以,若是你把一个服务注册为Scoped,那么它能够在一个Web请求期间被共享。例如:

public class RequestItemsService
  {
      private readonly Dictionary<string, object> _items;
  
      public RequestItemsService()
      {
          _items = new Dictionary<string, object>();
      }  
     public void Set(string name, object value)
     {
         _items[name] = value;
     }
 
     public object Get(string name)
     {
         return _items[name];
     }
 }

若是你将RequestItemsService注册为Scoped,并注入到两个不一样的服务,而后你能够获得一个从另一个服务添加的项。由于它们会共享同一个RequestItemsService的实例。这就是咱们对 Scoped服务的预期

 

可是!!!事实并不老是如此。 若是你建立了一个子服务做用域并从子做用域解析RequestItemsService,而后你会获得一个RequestItemsService的新实例,而且不会按照你的预期工做。所以,Scoped服务并不老是意味着每一个Web请求一个实例。

你可能认为你不会犯如此明显的错误(在子做用域内部解析另外一个做用域)。可是,这并非一个错误(一个很常规的用法)而且状况可能不会如此简单。若是你的服务之间有一个大的依赖关系,你不知道是否有人建立了子做用域并在其余注入的服务中解析了服务……最终注入了一个Scoped服务。

实践指南:

  • Scoped服务能够认为是在Web请求中注入太多服务的一种优化。所以,在相同的Web请求期间,全部这些服务都将使用该服务的单个实例。
  • Scoped服务无需设计为线程安全的。由于,它们应该正常地被单个Web请求或线程使用。可是,这这种状况下,你不该该在不一样的线程之间共享服务做用域
  • 在Web请求中,若是你设计一个Scoped服务在其余服务之间共享数据,请当心(上面解释过)。你能够在HttpContext中存储每一个Web请求的数据(注入IHttpContextAccessor 来访问它),这是共享数据的更安全的方式。 HttpContext的生命周期不是Scoped类型的,事实上,它根本不会被注册到DI(这也是为何不注入它,而是注入 IHttpContextAccessor来代替)。HttpContextAccessor 的实现采用 AsyncLocal 在Web请求期间共享同一个 HttpContext.

结论:

依赖注入刚开始看起来很容易使用,可是若是你不遵循一些严格的原则,则会有潜在的多线程问题和内存泄漏问题。我分享的这些实践指南基于我在开发ABP框架期间的我的经验。

相关文章
相关标签/搜索