ASP.NET Core 依赖注入最佳实践与技巧

原文地址:https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96 [正(ke)确(xue)上(shang)网(wang)]

posted by Halil İbrahim Kalkan Jul 12, 2018 · 7 min read

在这篇文章中,我将分享一下在ASP.NET Core应用程序中使用依赖注入的经验与建议。

主要分享的目的,基于以下几点原则:

  • 有效的设计服务及它们的依赖关系
  • 预防多线程问题
  • 预防内存移除
  • 预防潜在bugs

这篇文章的前提假设你已经对依赖注入和ASP.NET Core由基本的认识,如果还没有,首先请阅读ASP.NET Core Dependency Injection documentation

基础

构造函数注入

构造函数注入(Constructor injection)用于声明和获取服务对服务构造的依赖关系。

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

ProductService在构造函数中注入了它的依赖IProductRepository,然后使用了它的Delete方法。

良好实践

  • 在服务构造函数中显式定义所需的依赖项。这样,服务缺失依赖关系就不能构造。
  • 将注入的依赖项赋值给一个只读(read only)字段/属性(防止在方法调用过程中无意的赋值了其他值)。

属性注入

ASP.NET Core的标配的依赖注入容器并不支持属性注入(property injection)。但是你可以使用其他的依赖注入容器支持属性注入。

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)属性。依赖注入容器能赋值一个可用的值给这个日志属性(前提是已经在依赖注入容器内注册过)。

良好实践

  • 仅对可选依赖项使用属性注入。这意味着你的服务可以在不提供这些依赖项的情况下正常工作。
  • 尽量使用空对象模式(如实例所示)。否则,在使用依赖项时始终做NULL检查。

服务定位(Service Locator)

服务定位(Service Locator)模式是另一种获取依赖项的方式。

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,并使用它解析了ProdProductServiService的依赖关系。如果在使用之前注入容器的话,使用GetRequiredService方法会抛异常。另一边,使用GetService则返回NULL。

当你在构造函数中解析(resolve)依赖服务时,他们随着服务本身的释放而释放,所以你大可不必关系构造函数注入的依赖项的释放(就像构造函数和属性注入一样)。

良好实践

  • 尽可能不要使用服务定位(Service Locator)模式。因为这样使得服务的依赖关系隐式化(译注,++服务的依赖关系不是显示的注入,导致代码层面的服务依赖关系不明确,从构造函数看,只有一个IServiceProvider的依赖++)。这意味着在创建服务实例时不能显示的看到服务的依赖项。而这对于单元测试尤其重要,因为你可能想要模拟服务的一些依赖项。
  • 尽可能使用构造函数解析服务依赖项。在服务方法中解析依赖项会让应用程序变得更复杂,更容易出错。接下来,我将介绍这些问题和解决方案。

服务生命周期

在ASP.NET Core依赖注入概念里面,有三种服务的生命周期:

  1. Transient服务,在请求或注入服务的时候,每次都创建新实例。
  2. Scoped服务,在作用域内创建服务。在Web应用程序,每一个web请求都会创建一个新的独立的服务作用域范围。这意味着每个web请求通常都创建有作用域的服务
  3. Singleton服务,每个依赖注入容器会创建一次单例服务。在每个应用程序只会创建一次单例服务,在应用的整个生命周期都可用。

依赖注入容器会跟踪所有解析出来的服务,在它们的生命周期结束后会释放掉这些服务。

  • 如果服务有依赖项,这些依赖项也会自动释放。
  • 如果服务已经实现了IDisposable接口,在服务被释放的时候也会自动调用Dispose方法。

良好实践

  • 尽可能的将你的服务注册成Transient服务。设计一个Transient服务是相对简单的,因为你通常不需要关心多线程和内存泄漏的问题,而且这些服务生命周期相对短。
  • 小心使用Scoped服务,因为当你创建子作用域或者在非web应用程序使用Scoped服务,会出现一些棘手的问题。
  • 小心使用Singleton服务,因为你需要正确处理多线程问题和潜在的内存泄露问题。
  • 不要在Singleton服务中依赖一个Transient服务或Scoped服务。因为这时Transient服务会变成Singleton服务,如果Transient服务不支持单例场景,当Singleton服务注入Transient服务时会产生异常问题。ASP.NET Core默认依赖注入容器在这种场景下会抛异常。

在方法内解析服务

在某些场景下,你可能需要在服务的方法中解析另外一个服务。这种情况下请确保在使用服务后及时释放服务。这才是创建范围作用域服务的最佳方式。

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并赋值给以只读字段。然后PriceCalculatorCalculate的方法内创建了一个子范围作用域。使用scope.ServiceProvider来解析服务依赖,而不是用_serviceProvider实例。这样,在子范围作用域内被解析的所有服务会在using的声明结束后自动释放。

良好实践

  • 如果在方法内解析服务,请始终创建子范围作用域,以确保已解析的服务被正确释放。
  • 如果一个方法使用IServiceProvider作为参数,那么可以直接使用它解析服务依赖,而不需要关心依赖服务是否释放。创建/管理服务范围作用域是调用方法代码的职责。遵循这一原则可以使代码更简洁。
  • 不要保存对已解析服务的引用!否则,在使用对象引用时访问已释放的服务可能会导致内存泄漏(除非已解析的服务是单例的)。

单例服务 Singleton Services

单例服务通常为了保持应用程序状态而设计。缓存是一个应用程序状态的最好示例。

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来确保线程安全。
  • 不要在单例服务内使用Scoped/Transient服务,因为Transient服务可能不是线程安全的设计。如果确实需要使用,请注意多线程(例如使用Lock)。
  • 引起内存泄漏的通常是由单例服务引起的。在应用程序结束之前,单例服务不会被释放。它们实例化类(或注入实例)也不会提前被释放,它们也会一直留在内存中,直到应用程序结束。确保在适当的时候释放服务,请参阅在方法内解析服务
  • 如果使用缓存数据(例如上述代码示例中文件内容的缓存),应该创建一种机制当原始数据发生变更的时候去更新或淘汰已缓存的数据(示例中当磁盘的文件变更时应该更新缓存)。

范围作用域服务Scoped Services

范围作用域服务似乎是一个为每个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注册成范围作用域的服务,并将RequestItemsService注入到两个不同的服务中,这两个服务可以访问到另外一个服务添加的数据,因为这两个服务在一个Web请求中是共享RequestItemsService实例的。

但是,现实情况可能不完全是这样的。如果你创建了子范围作用域并在子作用域范围内解析RequestItemsService,你会得到一个全新的RequestItemsService,而这并非我们所期望的那样。所有Scoped服务并非一个Web请求时共享一个服务实例。

你可能会认为你不会犯这样明显的错误(在子作用域内解析服务依赖)。但是这不是错误(一个常规用法而已)并且情况并没有那么简单。假设在你的服务中有庞大的服务依赖关系,你可能不知道是否有人会这么做(在子作用域内解析服务依赖)。

良好实践

  • Scoped服务可以视作一种优化手段(在一个web请求中不想注入太多服务)。这样在同一个Web请求中所有的服务使用同一个实例。
  • Scoped服务不需要设计线程安全。因为Scoped服务通常在一个线程或Web请求中使用,但是,这种场景下,不应该在不同线程之间共享Scoped服务。
  • 如果要设计一个作用域服务来在web请求中的其他服务之间共享数据,小心上述问题。你可以使用HttpContext(通过IHttpContextAccessor来访问它)来存储每一个Web请求需要存储的数据,这是安全的处理方式。HttpContext生命周期并不是Scoped。实际上并没有注入到依赖注入的容器内(这是为什么使用IHttpContextAccessor访问它而不是注入到容器内的原因)。++在一个Web请求中,HttpContextAccessor使用AsyncLocal来共享相同的HttpContext++。

结论

依赖注入在最初使用的时候好像是挺简单的。如果不遵循严格的使用原则,依然会有潜在的多线程和内存泄漏问题。我在开发ASP.NET Boilerplate框架过程中,基于我的实践体会分享了这些实践原则。


总结

在使用ASP.NET Core 依赖注入时需要注意几项:

  1. 在构造函数中显示的注入依赖关系。
  • 在依赖关系众多时,职责单一原则,考虑拆分职责
  • 更有利于单元测试。
  1. 属性注入,适用于可选依赖项,不影响服务正常运行,考虑空实现模式。
  • 通常我们在设计框架/基类时,可以适当引入属性注入,这样可以使得继承类代码更简洁。
  • 必要时,属性提供懒加载方式,提高服务启动速度。
  1. 选择合适的服务生命周期。顺序依次Transient > Singleton > Scoped,不确定时使用Transient ,明确使用场景的时候考虑Singleton和Scoped。同需要需要考虑服务的构建成本。
  • Transient服务的生命周期短,可以有效的规避多线程和内存泄漏问题,同时也引起应用程序的内存使用量上升,带了部分性能问题。
  • 在Singleton服务中,禁止依赖Transient/Scoped服务,一方面,Transient/Scoped服务也会变成单例服务。另一方面,Transient/Scoped服务没有考虑多线程问题。
  • 在使用Singleton服务时,多注意潜在的线程安全和内存泄漏问题。
  • 在非Web应用场景和子作用服务场景,Scoped服务,并不能正确处理一个线程内共享实例。

ASP.NET Core 依赖注入最佳实践与技巧的更多相关文章

  1. ASP.NET Core依赖注入最佳实践,提示&技巧

    分享翻译一篇Abp框架作者(Halil İbrahim Kalkan)关于ASP.NET Core依赖注入的博文. 在本文中,我将分享我在ASP.NET Core应用程序中使用依赖注入的经验和建议. ...

  2. ASP.NET Core 依赖注入最佳实践——提示与技巧

    在这篇文章,我将分享一些在ASP.NET Core程序中使用依赖注入的个人经验和建议.这些原则背后的动机如下: 高效地设计服务和它们的依赖. 预防多线程问题. 预防内存泄漏. 预防潜在的BUG. 这篇 ...

  3. ASP.NET Core依赖注入——依赖注入最佳实践

    在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用 ...

  4. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  5. ASP.NET Core 性能优化最佳实践

    本文提供了 ASP.NET Core 的性能最佳实践指南. 译文原文地址:https://docs.microsoft.com/en-us/aspnet/core/performance/perfor ...

  6. [译]ASP.NET Core依赖注入深入讨论

    原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog 这篇文章我们来深入探讨ASP.NET Core.MVC Core中 ...

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

    原文:ASP.NET Core 依赖注入(构造函数注入,属性注入等) 如果你不熟悉ASP.NET Core依赖注入,先阅读文章: 在ASP.NET Core中使用依赖注入   构造函数注入 构造函数注 ...

  8. # ASP.NET Core依赖注入解读&使用Autofac替代实现

    标签: 依赖注入 Autofac ASPNETCore ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Aut ...

  9. 实现BUG自动检测 - ASP.NET Core依赖注入

    我个人比较懒,能自动做的事绝不手动做,最近在用ASP.NET Core写一个项目,过程中会积累一些方便的工具类或框架,分享出来欢迎大家点评. 如果以后有时间的话,我打算写一个系列的[实现BUG自动检测 ...

随机推荐

  1. LTE无线网络优化简介

    LTE无线网络优化特点 覆盖和质量的估计参数不同 TD-LTE使用RSPP.RSRQ.SINR进行覆盖和质量的评估. 影响覆盖问题的因素不同 工作频段的不同,导致覆盖范围的差异显著:需要考虑天线模式对 ...

  2. Vue实现靠边悬浮球(PC端)

    我想把退出登录的按钮做成一个悬浮球的样子,带动画的那种. 实现是这个样子: 手边没有球形图.随便找一个,功能这里演示的为单机悬浮球注销登录 嗯,具体代码: <div :class="[ ...

  3. webpack搭建环境步骤

    一.初始化 1.创建文件夹 2.npm init  -y 二.安装webpack 和webpack-cli 1.yarn add webpack webpack-cli@3.3.10 -D (这里指定 ...

  4. Coursera课程笔记----计算导论与C语言基础----Week 7

    C语言中的数据成分(Week7) 内存 把内存想象成长带,带子上有许多方格,每个方格有8位(8bit) 2^10 = 1024 1B = 8 b 1KB = 1024Byte MB.GB.TB.PB- ...

  5. Redis 学习笔记(一) 字符串 SDS

    SDS 简单动态字符串. SDS的结构: struct sdshdr{ int len;//记录BUF数组中已使用字节的数量 ,等于SDS所八寸字符串的长度 int free;//记录BUF数组中未使 ...

  6. 超过百万的StackOverflow Flutter 问题-第二期

    老孟导读:一个月前分享的<超过百万的StackOverflow Flutter 问题-第一期>受到很多朋友的喜欢,非常感谢大家的支持,在文章末尾有第一期的链接,希望此文能对你有所帮助. N ...

  7. android 压缩图片大小,防止OOM

    android开发中,图片的处理是非常普遍的,经常是需要将用户选择的图片上传到服务器,但是现在手机的分辨率越来越好了,随便一张照片都是2M或以上,如果直接显示到ImageView中,是会出现OOM的, ...

  8. vue 升级element-ui woff文件404

    一.build文件下utils.js下增加 publicPath:'../../' 二. 同样的代码环境,用yarn来安装依赖后启动运行正常,而采用npm安装依赖则有类似问题.当然,这个和yarn或者 ...

  9. VST的安装

    对需要使用VST的用户,你可以到http://www.soft-gems.net/去免费下载没有使用限制.没有广告的VST.包括例子程序以及说明文档也可以下载到,下载完成后,就是安装,以前版本的VST ...

  10. 【Python】【第二节】【时间与日期处理模块】

    转载至https://blog.csdn.net/p9bl5bxp/article/details/54945920 Python中提供了多个用于对日期和时间进行操作的内置模块:time模块.date ...