从小白到大神——.NET下理解注入服务的作用域概念
引言
嘿,各位码农朋友们!今天我们要聊的话题是.NET中的依赖注入(Dependency Injection,简称DI)服务的作用域概念。别急着关掉页面,我知道你们中的一些人可能已经在心里默念:“又是依赖注入,老生常谈了吧!” 但别急,今天我们要用一种全新的视角来理解这个话题,保证让你从“小白”秒变“大神”。
1.依赖注入:从“小白”到“大神”的第一步
1.1 什么是依赖注入?
首先,让我们回顾一下依赖注入的基本概念。依赖注入是一种设计模式,它允许我们将对象的创建和依赖关系的管理从代码中分离出来。简单来说,就是让你的代码更加灵活、可测试和可维护。
在.NET中,依赖注入是通过IServiceProvider
接口和ServiceCollection
类来实现的。我们可以通过ServiceCollection
来注册服务,然后通过IServiceProvider
来获取这些服务的实例。
1.2 为什么需要依赖注入?
想象一下,你正在开发一个电商网站。你的购物车服务依赖于库存服务和支付服务。如果没有依赖注入,你可能会这样写代码:
代码语言:javascript代码运行次数:0运行复制public classShoppingCart
{
privatereadonlyInventoryService _inventoryService;
privatereadonlyPaymentService _paymentService;
publicShoppingCart()
{
_inventoryService =newInventoryService();
_paymentService =newPaymentService();
}
publicvoidCheckout()
{
// 检查库存
_inventoryService.CheckStock();
// 处理支付
_paymentService.ProcessPayment();
}
}
这段代码看起来没什么问题,但它有一个致命的缺点:ShoppingCart类直接依赖于InventoryService和PaymentService的具体实现。这意味着如果你想替换InventoryService或PaymentService的实现,或者想在单元测试中模拟这些服务,你将不得不修改ShoppingCart类的代码。
依赖注入就是为了解决这个问题而生的。通过依赖注入,我们可以将ShoppingCart类的依赖关系从代码中分离出来,让它们可以在运行时动态注入。
代码语言:javascript代码运行次数:0运行复制public classShoppingCart
{
privatereadonlyIInventoryService _inventoryService;
privatereadonlyIPaymentService _paymentService;
publicShoppingCart(IInventoryService inventoryService,IPaymentService paymentService)
{
_inventoryService = inventoryService;
_paymentService = paymentService;
}
publicvoidCheckout()
{
// 检查库存
_inventoryService.CheckStock();
// 处理支付
_paymentService.ProcessPayment();
}
}
现在,ShoppingCart
类不再直接依赖于InventoryService
和PaymentService
的具体实现,而是依赖于它们的接口IInventoryService
和IPaymentService
。这样,我们就可以在运行时动态注入不同的实现,而不需要修改ShoppingCart
类的代码。
2.服务的作用域:从“小白”到“大神”的第二步
什么是服务的作用域?
在.NET中,服务的作用域决定了服务的生命周期和实例化方式。.NET提供了三种服务作用域:
- Singleton(单例):整个应用程序生命周期内只创建一个实例。
- Scoped(作用域):在每个请求或作用域内创建一个实例。
- Transient(瞬时):每次请求时都创建一个新的实例。
2.1 Singleton(单例)
单例服务在整个应用程序生命周期内只创建一个实例。这意味着无论你在哪里请求这个服务,你都会得到同一个实例。
代码语言:javascript代码运行次数:0运行复制services.AddSingleton<IInventoryService, InventoryService>();
单例服务非常适合那些无状态的服务,或者那些需要在整个应用程序中共享状态的服务。例如,配置服务、日志服务等。
注意: 单例可以替换旧代码中的大部分的静态类。
2.2 Scoped(作用域)
作用域服务在每个请求或作用域内创建一个实例。这意味着在同一个请求或作用域内,你每次请求这个服务都会得到同一个实例,但在不同的请求或作用域内,你会得到不同的实例。
代码语言:javascript代码运行次数:0运行复制services.AddScoped<IPaymentService, PaymentService>();
作用域服务非常适合那些需要在同一个请求或作用域内共享状态的服务。例如,数据库上下文、用户会话等。
2.3 Transient(瞬时)
瞬时服务每次请求时都创建一个新的实例。这意味着无论你在哪里请求这个服务,你都会得到一个新的实例。
代码语言:javascript代码运行次数:0运行复制services.AddTransient<IEmailService, EmailService>();
瞬时服务非常适合那些无状态的服务,或者那些不需要共享状态的服务。例如,邮件服务、通知服务等。
3.进阶:理解服务作用域的实际应用
3.1 场景一:单例服务的陷阱
假设我们有一个单例服务CounterService,它用于计数:
代码语言:javascript代码运行次数:0运行复制public classCounterService
{
privateint _count =;
publicintIncrement()
{
return++_count;
}
}
我们在Startup.cs中注册这个服务:
services.AddSingleton<CounterService>();
然后我们在控制器中使用这个服务:
代码语言:javascript代码运行次数:0运行复制public class HomeController : Controller
{
private readonly CounterService _counterService;
public HomeController(CounterService counterService)
{
_counterService = counterService;
}
public IActionResult Index()
{
ViewBag.Count = _counterService.Increment();
return View();
}
}
有啥问题吗?
看起来没什么问题,对吧?但是,如果我们有多个用户同时访问这个页面,会发生什么呢?由于CounterService
是单例的,所有的用户都会共享同一个CounterService
实例,这会导致计数器的值被多个用户同时修改,从而产生竞争条件。
3.2 场景二:作用域服务的优势
为了避免上述问题,我们可以将CounterService注册为作用域服务: 这样,每个请求都会有一个独立的CounterService实例,不同用户之间的计数器不会相互干扰。
3.3 场景三:瞬时服务的灵活性
假设我们有一个邮件服务EmailService,它用于发送邮件:
代码语言:javascript代码运行次数:0运行复制public classEmailService
{
publicvoidSendEmail(string to,string subject,string body)
{
// 发送邮件的逻辑
}
}
services.AddTransient<EmailService>();
我们在控制器中使用这个服务:
代码语言:javascript代码运行次数:0运行复制public classHomeController:Controller
{
privatereadonlyEmailService _emailService;
publicHomeController(EmailService emailService)
{
_emailService = emailService;
}
publicIActionResultSendWelcomeEmail()
{
_emailService.SendEmail("webmote@gmail","Welcome","Welcome to our website!");
returnView();
}
}
由于EmailService是瞬时的,每次请求时都会创建一个新的实例。这意味着我们可以在不同的请求中使用不同的EmailService实例,而不需要担心它们之间的状态冲突。
4.进阶:服务作用域的嵌套与生命周期管理
嵌套作用域 在.NET中,我们可以创建嵌套的作用域。嵌套作用域允许我们在一个请求或作用域内创建另一个独立的作用域。这在某些场景下非常有用,例如在处理后台任务或并行操作时。
代码语言:javascript代码运行次数:0运行复制using (var scope = serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<ScopedService>();
// 使用scopedService
}
在这个例子中,我们创建了一个新的作用域,并在该作用域内获取了一个ScopedService实例。这个实例只在该作用域内有效,当作用域结束时,它会被自动释放。
生命周期管理
在.NET中,服务的生命周期由IServiceProvider
管理。当我们注册服务时,我们可以指定服务的生命周期。IServiceProvider
会根据服务的生命周期来管理服务的创建和释放。
Singleton
:服务实例在第一次请求时创建,并在应用程序关闭时释放。Scoped
:服务实例在作用域开始时创建,并在作用域结束时释放。Transient
:服务实例在每次请求时创建,并在请求结束时释放。
注意事项
避免单例服务依赖作用域服务:单例服务的生命周期比作用域服务长,如果单例服务依赖作用域服务,会导致作用域服务在单例服务中无法正确释放,从而引发内存泄漏。
避免作用域服务依赖瞬时服务:作用域服务的生命周期比瞬时服务长,如果作用域服务依赖瞬时服务,会导致瞬时服务在作用域服务中无法正确释放,从而引发内存泄漏。
避免循环依赖:循环依赖是指两个或多个服务相互依赖,导致无法正确解析服务实例。在.NET中,循环依赖会导致InvalidOperationException
异常。
5.进阶:自定义服务作用域
在某些情况下,我们可能需要自定义服务的作用域。例如,我们可能希望某个服务在特定的条件下才创建实例,或者在特定的条件下才释放实例。
自定义服务工厂
我们可以使用AddSingleton
、AddScoped
和AddTransient
方法的另一个重载来注册自定义服务工厂。自定义服务工厂允许我们控制服务的创建过程。
services.AddScoped<IService>(serviceProvider =>
{
// 自定义服务创建逻辑
return new CustomService();
});
在这个例子中,我们使用了一个自定义的工厂方法来创建CustomService
实例。这样,我们就可以在服务创建时执行一些自定义逻辑。
自定义服务释放
我们可以通过实现IDisposable
接口来自定义服务的释放逻辑。当服务的作用域结束时,IServiceProvider
会自动调用服务的Dispose
方法。
public class CustomService : IDisposable
{
public void Dispose()
{
// 自定义服务释放逻辑
}
}
在这个例子中,我们实现了IDisposable
接口,并在Dispose
方法中定义了自定义的释放逻辑。当CustomService
实例的作用域结束时,Dispose
方法会被自动调用。
6.进阶:服务作用域与异步编程
在异步编程中,服务作用域的管理变得更加复杂。由于异步操作可能会跨越多个线程,我们需要确保在异步操作中正确管理服务的作用域。
异步作用域
在异步操作中,我们可以使用AsyncLocal<T>
来管理服务的作用域。AsyncLocal<T>
允许我们在异步操作中共享数据,而不需要担心线程切换导致的数据丢失。
public classAsyncScopeService
{
privatestaticreadonlyAsyncLocal<IServiceProvider> _asyncLocal =newAsyncLocal<IServiceProvider>();
publicstaticIServiceProvider Current
{
get=> _asyncLocal.Value;
set=> _asyncLocal.Value =value;
}
}
在这个例子中,我们使用AsyncLocal<IServiceProvider>
来存储当前的作用域。这样,我们就可以在异步操作中访问当前的作用域,而不需要担心线程切换导致的作用域丢失。
异步服务释放
在异步操作中,我们需要确保服务在异步操作结束时正确释放。我们可以使用using语句来确保服务在异步操作结束时自动释放。
public async Task DoSomethingAsync()
{
using (var scope = serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<ScopedService>();
await scopedService.DoSomethingAsync();
}
}
在这个例子中,我们使用using语句来创建一个新的作用域,并在异步操作结束时自动释放作用域。这样,我们就可以确保服务在异步操作结束时正确释放。
7 进阶:服务作用域与AOP(面向切面编程)
AOP(Aspect-Oriented Programming)是一种编程范式,它允许我们将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来。在.NET中,我们可以使用依赖注入来实现AOP
。
AOP与服务作用域
在AOP
中,我们通常会在方法执行前后执行一些横切逻辑。这些横切逻辑可能需要访问服务实例,因此我们需要确保在AOP
中正确管理服务的作用域。
public classLoggingAspect:IInterceptor
{
privatereadonlyILogger _logger;
publicLoggingAspect(ILogger logger)
{
_logger = logger;
}
publicvoidIntercept(IInvocation invocation)
{
_logger.LogInformation($"Before {invocation.Method.Name}");
invocation.Proceed();
_logger.LogInformation($"After {invocation.Method.Name}");
}
}
在这个例子中,我们实现了一个LoggingAspect
,它会在方法执行前后记录日志。由于LoggingAspect
依赖于ILogger
服务,我们需要确保在AOP
中正确管理ILogger
服务的作用域。
AOP与服务作用域的管理
在AOP
中,我们可以使用IServiceProvider
来获取服务实例,并确保在AOP
中正确管理服务的作用域。
public classLoggingAspect:IInterceptor
{
privatereadonlyIServiceProvider _serviceProvider;
publicLoggingAspect(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
publicvoidIntercept(IInvocation invocation)
{
using(var scope = _serviceProvider.CreateScope())
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger>();
logger.LogInformation($"Before {invocation.Method.Name}");
invocation.Proceed();
logger.LogInformation($"After {invocation.Method.Name}");
}
}
}
在这个例子中,我们使用IServiceProvider
来创建一个新的作用域,并在AOP中正确管理ILogger
服务的作用域。这样,我们就可以确保在AOP中正确管理服务的作用域。
8.进阶:Scope 以及嵌套Scope
注意: core原生的作用域是没有作用域树/嵌套的概念的!
Scope的每个作用域都是一样的,他们均由根容器创建,一个父作用域创建了一个子作用域,如果父作用域释放了,并不影响子作用域的使用!
例如:
代码语言:javascript代码运行次数:0运行复制 public classScopeB{
publicScopeB(ScopeA a)
{
A = a;
}
publicScopeA A {get;}
}
------------------------------------
var sc =newServiceCollection();
sc.AddScoped<ScopeA>();
sc.AddScoped<ScopeB>();
sc.AddScoped<ScopeC>();
var sp = sc.BuildServiceProvider();
IServiceScope scope2;
ScopeA a2;
using(var scope1 = sp.CreateScope())
{
var a1 = scope1.ServiceProvider.GetService<ScopeA>();
var b1 = scope1.ServiceProvider.GetService<ScopeB>();
scope2 = sp.CreateScope();
{
a2 = scope2.ServiceProvider.GetService<ScopeA>();
var b2 = scope2.ServiceProvider.GetService<ScopeB>();
var scope3= sp.GetService<IServiceScopeFactory>().CreateScope();
var same =object.ReferenceEquals(a1, a2);
Console.WriteLine($"a1==a2? {same}");
Console.WriteLine($"b1.A==a1? {object.ReferenceEquals(b1.A, a1)}");
Console.WriteLine($"b1.A==b2.A? {object.ReferenceEquals(b1.A, b2.A)}");
}
}
var a3 = scope2.ServiceProvider.GetService<ScopeA>();
Console.WriteLine($"a2==a3? {object.ReferenceEquals(a2,a3)}");
输出结果是,scope1结束释放了,scope2并没有自动释放。
代码语言:javascript代码运行次数:0运行复制a1==a2? False
b1.A==a1? True
b1.A==b2.A? False
a2==a3? True
多个Scope 的关系类似如图所示。
这里有个问题,你来思考下!
如果一个注册为瞬时周期的类A, 需要使用Scope1内的类
ScopeA
,怎么设计她们的关系?
想到了吗? 这里有两个方案提供给你:
- 保持瞬态类A的创建来自于
Scope1
,那么其直接获取或注入的ScopeA
,就来自Scope1
,那么我们可直接访问。 - 瞬态类A注入一个单例类B,这个单例类B维持一个可以访问
Scope1
的IServiceScope
实例, 通过访问Scope1
的ServiceProvider
获取ScopeA
, 那这个ScopeA既然来自Scope1范围,那么它既是我们要找的对象。