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

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

 

构造函数注入

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

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

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

实践指南:

  • 在服务构造函数中明确地定义必需的依赖。因此该服务在没有这些依赖时无法被构造。
  • 将注入的依赖赋值给只读(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。

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

实践指南

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

服务生命周期

 下面是服务在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框架期间的个人经验。

ASP.NET Core 依赖注入(构造函数注入,属性注入等)的更多相关文章

  1. ASP.NET Core中使用Autofac进行属性注入

    一些无关紧要的废话: 作为一名双修程序员(自封的),喜欢那种使用Spring的注解形式进行依赖注入或者Unity的特性形式进行依赖注入,当然,形式大同小异,但结果都是一样的,通过属性进行依赖注入. A ...

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

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

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

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

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

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

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

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

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

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

  7. ASP.NET Core依赖注入解读&使用Autofac替代实现【转载】

    ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Autofac实现和自定义实现扩展方法 3.1 安装Autof ...

  8. ASP.NET Core 依赖注入基本用法

    ASP.NET Core 依赖注入 ASP.NET Core从框架层对依赖注入提供支持.也就是说,如果你不了解依赖注入,将很难适应 ASP.NET Core的开发模式.本文将介绍依赖注入的基本概念,并 ...

  9. 任务21 :了解ASP.NET Core 依赖注入,看这篇就够了

    DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例 ...

随机推荐

  1. linux中添加一个用户到指定用户组的两种方式,修改一个用户到指定用户组的一种方式

    添加一个用户到指定用户组: gpasswd –a 用户名 组名usermod –G 组名 用户名 //第一种:gpasswd –a 用户名 组名 [root@localhost ~]# id user ...

  2. dubbo---------timeout与retires

    相信很多人都见过这张图,这张图说明了提供者与消费者之间的关系,下面就介绍一下这个图是什么意思. 1.角色解释: Provider: 暴露服务的服务提供者. Consumer: 调用远程服务的服务消费者 ...

  3. LeetCode(136) Single Number

    题目 Given an array of integers, every element appears twice except for one. Find that single one. Not ...

  4. LeetCode(125) Valid Palindrome

    题目 Given a string, determine if it is a palindrome, considering only alphanumeric characters and ign ...

  5. 《C/C++专项练习》— (1)

    前言 每每到了一周之计的Monday啊,精神总是不佳,写篇博客提提神儿吧~ 继上次完成<C/C++工程师综合练习卷>后,有事儿没事儿就想刷几道题,赶脚不错,巩固了不少基础知识呢,要坚持哦~ ...

  6. Oracle中创建触发器示例及注意事项

    1.oracle 中创建触发器示例 CREATE TABLE "CONCEPT"."FREQUENCYMODIFYLOG" ( "FREQUENCYI ...

  7. 工作记录:maven远程下载的问题

    最近使用maven的时候遇到一个问题,每次构建都从https://repository.apache.org/snapshots仓库下载一些maven-metadata.xml,但是公司的环境又连不上 ...

  8. HDU-5423 Rikka with Tree。树深搜

    Rikka with Tree 题意:给出树的定义,给出树相似的定义和不同的定义,然后给出一棵树,求是否存在一颗树即和其相似又与其不同.存在输出NO,不存在输出YES. 思路:以1号节点为根节点,我们 ...

  9. Python之静态语法检查

    Python是一门动态语言.在给python传参数的时候并没有严格的类型限制.写python程序的时候,发现错误经常只能在执行的时候发现.有一些错误由于隐藏的比较深,只有特定逻辑才会触发,往往导致需要 ...

  10. 【Luogu】P2331最大子矩阵(DP)

    题目链接 这题的状态转移方程真是粗鄙. f[i][j][k]表示前i行用了j个矩阵状态为k的时候的最大值. k=0:两列都不选. k=1:取左弃右. k=2:选右弃左. k=3:左右都选,但分属于两个 ...