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

在这篇文章,我将分享一些在ASP.NET Core程序中使用依赖注入的个人经验和建议。这些原则背后的动机如下:

  • 高效地设计服务和它们的依赖。
  • 预防多线程问题。
  • 预防内存泄漏。
  • 预防潜在的BUG。

这篇文章假设你已经基本熟悉依赖注入和ASP.NET Core。如果不是,则先阅读文章: 在ASP.NET Core中使用依赖注入

基础

构造函数注入

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

 1 public class ProductService
 2 {
 3     private readonly IProductRepository _productRepository;
 4     public ProductService(IProductRepository productRepository)
 5     {
 6         _productRepository = productRepository;
 7     }
 8     public void Delete(int id)
 9     {
10         _productRepository.Delete(id);
11     }
12 }

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

实践指南:

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

属性注入

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

 1 using Microsoft.Extensions.Logging;
 2 using Microsoft.Extensions.Logging.Abstractions;
 3 namespace MyApp
 4 {
 5     public class ProductService
 6     {
 7         public ILogger<ProductService> Logger { get; set; }
 8         private readonly IProductRepository _productRepository;
 9         public ProductService(IProductRepository productRepository)
10         {
11             _productRepository = productRepository;
12             Logger = NullLogger<ProductService>.Instance;
13         }
14         public void Delete(int id)
15         {
16             _productRepository.Delete(id);
17             Logger.LogInformation(
18                 $"Deleted a product with id = {id}");
19         }
20     }
21 }

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

依赖注入容器可以设置 Logger属性,如果它可用(已经注册到DI容器)。

实践指南:

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

服务定位器

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

 1 public class ProductService
 2 {
 3     private readonly IProductRepository _productRepository;
 4     private readonly ILogger<ProductService> _logger;
 5     public ProductService(IServiceProvider serviceProvider)
 6     {
 7         _productRepository = serviceProvider
 8           .GetRequiredService<IProductRepository>();
 9         _logger = serviceProvider
10           .GetService<ILogger<ProductService>>() ??
11             NullLogger<ProductService>.Instance;
12     }
13     public void Delete(int id)
14     {
15         _productRepository.Delete(id);
16         _logger.LogInformation($"Deleted a product with id = {id}");
17     }
18 }

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容器在这种情况下会抛出异常

在方法体中解析服务

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

 1 public class PriceCalculator
 2 {
 3     private readonly IServiceProvider _serviceProvider;
 4     public PriceCalculator(IServiceProvider serviceProvider)
 5     {
 6         _serviceProvider = serviceProvider;
 7     }
 8     public float Calculate(Product product, int count,
 9       Type taxStrategyServiceType)
10     {
11         using (var scope = _serviceProvider.CreateScope())
12         {
13             var taxStrategy = (ITaxStrategy)scope.ServiceProvider
14               .GetRequiredService(taxStrategyServiceType);
15             var price = product.Price * count;
16             return price + taxStrategy.CalculateTax(price);
17         }
18     }
19 }

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

实践指南:

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

Singleton服务

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

 1 public class FileService
 2 {
 3     private readonly ConcurrentDictionary<string, byte[]> _cache;
 4     public FileService()
 5     {
 6         _cache = new ConcurrentDictionary<string, byte[]>();
 7     }
 8     public byte[] GetFileContent(string filePath)
 9     {
10         return _cache.GetOrAdd(filePath, _ =>
11         {
12             return File.ReadAllBytes(filePath);
13         });
14     }
15 }

FileService简单地缓存了文件内容以减少磁盘读取。这个服务应该被注册为一个单例,否则,缓存将无法按照预期工作。

实践指南:

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

Scoped 服务

Scoped 生命周期的服务看起来是一个不错的存储每个Web请求数据的好方法。因为ASP.NET Core为每个Web请求创建一个服务作用域。因此,如果你把一个服务注册为Scoped,那么它可以在一个Web请求期间被共享。例如:

 1 public class RequestItemsService
 2 {
 3     private readonly Dictionary<string, object> _items;
 4
 5     public RequestItemsService()
 6     {
 7         _items = new Dictionary<string, object>();
 8     }
 9
10     public void Set(string name, object value)
11     {
12         _items[name] = value;
13     }
14
15     public object Get(string name)
16     {
17         return _items[name];
18     }
19 }

如果你将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框架期间的个人经验。

原文地址:https://www.cnblogs.com/WinHEC/p/Dependency-Injection-Best-Practices.html

时间: 2024-11-05 23:21:00

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

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

在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用中使用依赖注入的一些经验和建议,并且将会讨论这些原则背后的动机是什么: (1)有效地设计服务及其依赖关系. (2)防止多线程问题. (3)防止内存泄漏. (4)防止潜在的错误. 在讨论该话题之前,了解什么是服务是生命周期至关重要,当组件通过依赖注入请求另一个组件时,它接收的实例是否对该组件实例是唯一

asp.net core 依赖注入

依赖注入入门 全面理解 ASP.NET Core 依赖注入 参考https://www.cnblogs.com/tcjiaan/p/8732848.html 如何在StartUp中的ConfigureServices方法里直接调用刚刚添加好的注册? // redis注入 services.AddSingleton<IRedisConnection>(k => { return new RedisConnection(6, Configuration["RedisConnecti

ASP.NET Core 依赖注入(DI)

原文:ASP.NET Core 依赖注入(DI) ASP.NET Core的底层设计支持和使用依赖注入.ASP.NET Core 应用程序可以利用内置的框架服务将服务注入到启动类的方法中,并且应用程序服务也可以配置注入.由ASP.NET Core 提供的默认服务容器提供了最小功能集,并不是取代其他容器. 1.浅谈依赖注入 依赖注入(Dependency injection,DI)是一种实现对象和依赖者之间松耦合的技术,将类用来执行其操作的这些对象以注入的方式提供给该类,而不是直接实例化依赖项或者

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

ASP.NET Core 依赖注入 ASP.NET Core从框架层对依赖注入提供支持.也就是说,如果你不了解依赖注入,将很难适应 ASP.NET Core的开发模式.本文将介绍依赖注入的基本概念,并结合代码演示如何在 ASP.NET Core中使用依赖注入. 什么是依赖注入? 百度百科对于依赖注入的介绍: 控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度.其中最常见的方式叫做依赖注入(Dependency I

[转]ASP.NET Core Web API 最佳实践指南

原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 转自 介绍# 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难道不认为创建一个能正常工作的项目还不够吗?同时这个项目不应该也是可维护和可读的吗? 事实证明,我们需要把更多的关注点放到我们项目的可读性和可维护性上.这背后的主要原因是我们或许不是这个项目的唯一编写者.一旦我们完成后,其他人也极有可能会加入到这里面来. 因此,我们应该把关注点放到哪里呢? 在

了解ASP.NET Core 依赖注入,看这篇就够了

DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例生命周期的管理(这个是经常面试会问到的问题).最后再给大家简单介绍一下在控制台以及Mvc下如何使用DI,以及如何把默认的Service Container 替换成Autofac. 一.什么是依赖注入 1.1 依赖 1.2 什么注入 为什么反转 何为容器 二..NET Core DI 2.1 实例的注

【转】ASP.NET Core 依赖注入

DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例生命周期的管理(这个是经常面试会问到的问题).最后再给大家简单介绍一下在控制台以及Mvc下如何使用DI,以及如何把默认的Service Container 替换成Autofac. 一.什么是依赖注入 1.1 依赖 1.2 什么注入 为什么反转 何为容器 二..NET Core DI 2.1 实例的注

全面理解 ASP.NET Core 依赖注入

DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例生命周期的管理(这个是经常面试会问到的问题).最后再给大家简单介绍一下在控制台以及Mvc下如何使用DI,以及如何把默认的Service Container 替换成Autofac. 我录了一些关于ASP.NET Core的入门视频:有兴趣的同学可以去看看.  http://www.cnblogs.co

asp.net core 依赖注入问题

最近.net core可以跨平台了,这是一个伟大的事情,为了可以赶上两年以后的跨平台部署大潮,我也加入到了学习之列.今天研究的是依赖注入,但是我发现一个问题,困扰我很久,现在我贴出来,希望可以有人帮忙解决或回复一下. 背景:我测试.net自带的依赖注入生命周期,一共三个:Transient.Scope.Single三种,通过一个GUID在界面展示,但是我发现scope和single的每次都是相同的,并且single实例的guid值每次都会改变. 通过截图可以看到scope和Single每次浏览器