ASP.NETCore依赖注⼊——依赖注⼊最佳实践
在这篇⽂章中,我们将深⼊研究.NET Core和ASP.NET Core MVC中的依赖注⼊,将介绍⼏乎所有可能的选项,依赖注⼊是ASP.Net Core 的核⼼,我将分享在ASP.Net Core应⽤中使⽤依赖注⼊的⼀些经验和建议,并且将会讨论这些原则背后的动机是什么:
(1)有效地设计服务及其依赖关系。
(2)防⽌多线程问题。
(3)防⽌内存泄漏。
(4)防⽌潜在的错误。
在讨论该话题之前,了解什么是服务是⽣命周期⾄关重要,当组件通过依赖注⼊请求另⼀个组件时,它接收的实例是否对该组件实例是唯⼀的取决于⽣命周期。因此,设置⽣存期决定了组件实例化的次数以及组件是否共享。
在ASP.Net Core 依赖注⼊有三种:
Transient :每次请求时都会创建,并且永远不会被共享。
Scoped :在同⼀个Scope内只初始化⼀个实例,可以理解为(每⼀个request级别只创建⼀个实例,同⼀个http request会在⼀个scope内)
Singleton :只会创建⼀个实例。该实例在需要它的所有组件之间共享。因此总是使⽤相同的实例。
DI容器跟踪所有已解析的组件,组件在其⽣命周期结束时被释放和处理:
如果组件具有依赖关系,则它们也会⾃动释放和处理。
如果组件实现IDisposable接⼝,则在组件释放时⾃动调⽤Dispose⽅法。
重要的是要理解,如果将组件A注册为单例,则它不能依赖于使⽤Scoped或Transient⽣命周期注册的组件。更⼀般地说:服务不能依赖于⽣命周期⼩于其⾃⾝的服务。
通常你希望将应⽤范围的配置注册为单例,数据库访问类,⽐如Entity Framework上下⽂被推荐以Scoped⽅式注⼊,以便可以重⽤连接。如果要并⾏运⾏的话,请记住Entity Framework上下⽂不能由两
个线程共享,如果需要,最好将上下⽂注册为Transient,然后每个服务都获得⾃⼰的上下⽂实例,并且可以并⾏运⾏。
建议的做法:
尽可能将您的服务注册为瞬态服务。因为设计瞬态服务很简单。您通常不⽤关⼼多线程和内存泄漏,并且您知道该服务的寿命很短。
1、请谨慎使⽤Scoped,因为如果您创建⼦服务作⽤域或从⾮Web应⽤程序使⽤这些服务,则可能会⾮常棘⼿。
2、谨慎使⽤singleton ,因为您需要处理多线程和潜在的内存泄漏问题。
3、在singleton 服务中不要依赖transient 或者scoped 服务,因为如果当⼀个singleton 服务注⼊transient服务,这个 transient服务就会变成⼀个singleton服务,并且如果transient服务不是为⽀持这种情况⽽设计的,则可能导致问题。在这种情况下,ASP.NET Core的默认DI容器已经抛出异常。
注册服务是ConfigureServices(IServiceCollection)在您Startup班级的⽅法中完成的。
以下是服务注册的⽰例:
services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));
该⾏代码添加DataService到服务集合中。服务类型设置为IDataService如此,如果请求该类型的实例,则它们将获得实例DataService。⽣命周期也设置为Transient,因此每次都会创建⼀个新实例。
ASP.NET Core提供了各种扩展⽅法,⽅便服务的注册,⼀下是最常⽤的⽅式,也是⽐较推荐的做法:
services.AddTransient<IDataService, DataService>();
简单吧,对于不同的⽣命周期,有类似的扩展⽅法,你可以猜测它们的名称。如果需要,你还可以注册单⼀类型(实现类型=服务类型)
services.AddTransient<DataService>();
services.AddTransient<DataService, DataService>();
在某些特殊情况下,您可能希望接管某些服务的实例化过程。在这种情况下,您可以使⽤下⾯的⽅法。例⼦:
services.AddTransient<IDataService, DataService>((ctx) =>
{
IOtherService svc = ctx.GetService<IOtherService>();
//IOtherService svc = ctx.GetRequiredService<IOtherService>();
return new DataService(svc);
});
单例组件的注⼊,可以这样做:
services.AddSingleton<IDataService>(new DataService());
有⼀个⾮常有意思的场景,DataService实现两个接⼝,如果我们这样做:
验证结果:
我们将会得到两个实例,如果我们想共享⼀个实例,可以这样做:
验证结果:
如果组件具有依赖项,则可以从服务集合构建服务提供程序并从中获取必要的依赖项:
IServiceProvider provider = services.BuildServiceProvider();
IOtherService otherService = provider.GetRequiredService<IOtherService>();
var dataService = new DataService(otherService);
services.AddSingleton<IDataService>(dataService);
services.AddSingleton<ISomeInterface>(dataService);
但我们⼀般不会这样使⽤,也不建议这样使⽤。
现在我们已经注册了我们的组件,我们可以转向实际使⽤它们,如下:
构造函数注⼊
构造函数注⼊⽤于在服务构造上声明和获取服务的依赖关系。例如:
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】字段/属性(防⽌在⽅法内意外地为其分配另外⼀个值),如果你的项⽬接⼊到sonar就会知道这是⼀个代码规范。
服务定位器
服务定位器是另外⼀种获取依赖项的模式,例如:
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。
解析构造函数中的服务时,将在释放服务时释放它们,所以,你不⽤关⼼释放/处理在构造函数中解析的服务(就像构造函数和属性注⼊⼀样)。
建议的做法:
(1)尽可能不使⽤服务定位器模式,因为该模式存在隐含的依赖关系,这意味着在创建服务实例时⽆法轻松查看依赖关系,但是该模式对单元测试尤为重要。
(2)如果可能,解析服务构造函数中的依赖项。解析服务⽅法会使您的应⽤程序更加复杂且容易出错。我将在下⼀节中介绍问题和解决⽅案。
再看⼀个综合的例⼦:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext ctx)
{
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}
在中间件中注⼊组件有三种不同的⽅法:
1、构造函数
2、调⽤参数
3、HttpContext.RequestServices
让我们看看这三种⽅式注⼊的使⽤:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace WebAppPerformance
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly IDataService _svc;
public LoggingMiddleware(RequestDelegate next, IDataService svc)
{
_next = next;
_svc = svc;
}
public async Task Invoke(HttpContext httpContext, IDataService svc2)
{
IDataService svc3 = httpContext.RequestServices.GetService<IDataService>();
Debug.WriteLine("Request starting");
await _next(httpContext);
Debug.WriteLine("Request complete");
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class LoggingMiddlewareExtensions
{
public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}
}
中间件在应⽤程序⽣命周期中仅实例化⼀次,因此通过构造函数注⼊的组件对于所有通过的请求都是相同的。如果IDataService被注册
为singleton,我们会在所有这些实例中获得相同的实例。
如果被注册为scoped,svc2并且svc3将是同⼀个实例,但不同的请求会获得不同的实例;如果在Transient 的情况下,它们都是不同的实例。
注意:我会尽量避免使⽤RequestServices,只有在中间件中才使⽤它。
MVC过滤器中注⼊:
但是,我们不能像往常⼀样在控制器上添加属性,因为它必须在运⾏时获得依赖关系。
我们有两个选项可以在控制器或action级别添加它:
[TypeFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}
// or
[ServiceFilter(typeof(TestActionFilter))]
public class HomeController : Controller
mvc实例
{
}
关键的区别在于,TypeFilterAttribute将确定过滤器依赖性是什么,通过DI获取它们,并创建过滤器。ServiceFilterAttribute试图从服务集合中到过滤器!
为了[ServiceFilter(typeof(TestActionFilter))]⼯作,我们需要更多配置:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestActionFilter>();
}
现在ServiceFilterAttribute可以到过滤器了。
如果要全局添加过滤器:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(mvc =>
{
mvc.Filters.Add(typeof(TestActionFilter));
});
}
这次不需要将过滤器添加到服务集合中,就像TypeFilterAttribute在每个控制器上添加了⼀个过滤器⼀样。
在⽅法体内解析服务
在某些情况下,您可能需要在⽅法中解析其他服务。在这种情况下,请确保在使⽤后释放服务。确保这⼀点的最佳⽅法是创建scoped服务,例如:
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 作为参数,那么可以直接从中解析出服务,不⽤关⼼服务的释放/销毁。创建/管理服务的scoped是调⽤你⽅法的代码的责任,所以遵循该原则能是你的代码更简洁。
不要引⽤已经解析的服务,否则会导致内存泄漏,并且当你后⾯使⽤了对象的引⽤时,将很有机会访问到已经销毁的服务(除⾮被解析的服务是⼀个单例)
单例服务
单例服务通常⽤来保存应⽤程序的状态,缓存是应⽤程序状态的⼀个很好的例⼦,例如:
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只是缓存⽂件内容以减少磁盘读取。此服务应注册为singleton。否则,缓存将⽆法按预期⼯作。
建议的做法:
如果服务保持状态,则应以线程安全的⽅式访问该状态。因为所有请求同时使⽤相同的服务实例,所以我使⽤ConcurrentDictionary⽽不是Dictionary来确保线程安全。
不要在单例服务中使⽤scoped和transient 服务,因为transient 服务可能不是线程安全的,如果必须使⽤它们,那么在使⽤这些服务时请注意多线程。
内存泄漏通常是单例服务导致的,因为它们将驻留在内存中,直到应⽤程序结束。所以请确保在合适的时间释放它们,可以参考在⽅法体内解析服务部分。
如果缓存数据(本⽰例中的⽂件内容),则应创建⼀种机制,以便在原始数据源更改时更新/使缓存的数据⽆效(当此⽰例中磁盘上的缓存⽂件发⽣更改时)。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。