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

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

Copy
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)。但是你可以使用其他的依赖注入容器支持属性注入。。

Copy
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace MyApp
{
public class ProductService
{
public ILogger Logger { get; set; }
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
Logger = NullLogger.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)模式是另一种获取依赖项的方式。

Copy
public class ProductService
{
private readonly IProductRepository _productRepository;
private readonly ILogger _logger;
public ProductService(IServiceProvider serviceProvider)
{
_productRepository = serviceProvider
.GetRequiredService();
_logger = serviceProvider
.GetService<ILogger>() ??
NullLogger.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依赖注入概念里面,有三种服务的生命周期:

Transient服务,在请求或注入服务的时候,每次都创建新实例。
Scoped服务,在作用域内创建服务。在Web应用程序,每一个web请求都会创建一个新的独立的服务作用域范围。这意味着每个web请求通常都创建有作用域的服务
Singleton服务,每个依赖注入容器会创建一次单例服务。在每个应用程序只会创建一次单例服务,在应用的整个生命周期都可用。
依赖注入容器会跟踪所有解析出来的服务,在它们的生命周期结束后会释放掉这些服务。

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

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

Copy
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 Services#
单例服务通常为了保持应用程序状态而设计。缓存是一个应用程序状态的最好示例。

Copy
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请求过程可以共享这个服务。

Copy
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];
}
}
深圳网站建设www.sz886.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值