在本文中,我将分享我在 ASP.NET Core 应用程序中使用依赖注入的经验和建议。这些原则背后的动机是:
本文假定您已经基本熟悉了依赖注入和 ASP.NET Core。如果不熟悉,请先阅读 ASP.NET Core Dependency 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 方法中使用它。
👍 良好的做法
在服务构造函数中明确定义所需的依赖关系。因此,如果没有依赖关系,就不能构造服务。
将注入的依赖关系分配给一个只读的字段/属性(以防止在方法中意外地将另一个值分配给它)。
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 是用 public setter 声明一个 Logger 属性。依赖注入容器可以设置 Logger,如果它是可用的(之前注册到 DI 容器)。
服务定位模式是获取依赖关系的另一种方式。例如:
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 核心依赖注入中,有三种服务寿命:
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 实例。它使用 scope.ServiceProvider 来解析服务,而不是使用注入的 _serviceProvider 实例。因此,所有从作用域解析的服务都会在使用语句结束时自动 released/released。
👍 良好的做法
Transient 服务一般是为了保持应用状态而设计的。缓存是应用状态的一个很好的例子。例如:
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 只是简单地缓存文件内容以减少磁盘读取。这个服务应该被注册为 Transient。否则,缓存将无法按照预期工作。
👍 良好的做法
首先 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 service 并不总是意味着每个 web 请求的实例。
你可能会认为你不会犯这样一个明显的错误(在子作用域中解析一个作用域)。但是,这并不是一个错误(一个非常常规的用法),情况可能并不那么简单。如果你的服务之间有一个大的依赖图,你就不能知道是否有人创建了一个子作用域,并解析了一个服务,而这个服务注入了另一个服务......最终注入了一个作用域服务。
👍 良好的做法
依赖注入的使用初看起来很简单,但如果不严格遵循一些原则,就会出现潜在的多线程和内存泄漏问题。