本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。
前面的文章中,在註冊服務時,統一使用了services.AddSingleton<TService, TImplementation>()
的形式來註冊服務,這個方法的具體含義是什麼?有沒有其他類似的方法?而且我們還有一個疑問,容器在構造服務時,服務的生命週期是怎麼樣的?服務被申請一次,就構造一次嗎?還是整個程序週期,只構造一次?如果我們要自定義每個服務有不同的生命週期時,又該怎麼做?下面,我們將一起探尋服務的生命週期。
生命週期#
註冊到容器時,服務的生命週期分為三種
生命週期 | 何時創建 | 調用方法 |
---|---|---|
瞬時 / Transient | 每次請求,都会創建一個新的實例 | AddTransient() |
範圍 / Scoped | 在指定的範圍內,第一次請求時會創建一個實例 重複請求時,會返回同一個實例 | AddScoped() |
單例 / Singleton | 在整個程序生命週期,只會創建一次 後續所有請求,都会返回同一個實例 | AddSingleton() |
參考實際代碼,我們可以更直觀的理解依賴注入中的三種生命週期,下面的代碼可以在微軟的 .NET 文檔中找到對應的文章 ——教程:在 .NET 中使用依賴注入。或者可以在這個倉庫中直接查看獲取源碼
// https://github.com/alva-lin/Articles.DI/tree/master/ConsoleApp1
// 01. 聲明接口以及實現類
public interface IOperation
{
string OperationId { get; }
}
public interface ITransientOperation : IOperation {}
public interface IScopedOperation : IOperation {}
public interface ISingletonOperation : IOperation {}
public class DefaultOperation
: ITransientOperation, IScopedOperation, ISingletonOperation
{
// 創建一個 Guid,取最後 4 個字符
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
// 02. 創建一個服務,它依賴上面幾個 Operation
public class OperationLogger
{
private readonly ITransientOperation _transientOperation;
private readonly IScopedOperation _scopedOperation;
private readonly ISingletonOperation _singletonOperation;
public OperationLogger(ITransientOperation transientOperation,
IScopedOperation scopedOperation,
ISingletonOperation singletonOperation)
{
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void LogOperations(string scope)
{
LogOperation(_transientOperation, scope, "Always different");
LogOperation(_scopedOperation, scope, "Changes only with scope");
LogOperation(_singletonOperation, scope, "Always the same");
}
private static void LogOperation<T>(T operation, string scope, string message)
where T : IOperation
{
Console.WriteLine($"{scope}: {typeof(T).Name,-19} [{operation.OperationId} {message,-23}]");
}
}
// 03. 註冊服務並啟動程序
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// 將三種 Operation 分別註冊為三種聲明週期
services.AddTransient<OperationLogger>()
.AddTransient<ITransientOperation, DefaultOperation>()
.AddScoped<IScopedOperation, DefaultOperation>()
.AddSingleton<ISingletonOperation, DefaultOperation>();
})
.Build();
ExemplifyScoping(host.Services, "Scope 1");
ExemplifyScoping(host.Services, "Scope 2");
await host.RunAsync();
static void ExemplifyScoping(IServiceProvider services, string scope)
{
using IServiceScope serviceScope = services.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
var logger = provider.GetRequiredService<OperationLogger>();
logger.LogOperations($"{scope}: Call 1 ...");
Console.WriteLine();
logger = provider.GetRequiredService<OperationLogger>();
logger.LogOperations($"{scope}: Call 2 ...");
Console.WriteLine();
}
運行上述代碼,可以獲得下面的結果
Scope 1: Call 1 ...: ITransientOperation [b672 Always different ]
Scope 1: Call 1 ...: IScopedOperation [afd8 Changes only with scope]
Scope 1: Call 1 ...: ISingletonOperation [21b3 Always the same ]
Scope 1: Call 2 ...: ITransientOperation [b6fc Always different ]
Scope 1: Call 2 ...: IScopedOperation [afd8 Changes only with scope]
Scope 1: Call 2 ...: ISingletonOperation [21b3 Always the same ]
Scope 2: Call 1 ...: ITransientOperation [ef31 Always different ]
Scope 2: Call 1 ...: IScopedOperation [46d1 Changes only with scope]
Scope 2: Call 1 ...: ISingletonOperation [21b3 Always the same ]
Scope 2: Call 2 ...: ITransientOperation [9864 Always different ]
Scope 2: Call 2 ...: IScopedOperation [46d1 Changes only with scope]
Scope 2: Call 2 ...: ISingletonOperation [21b3 Always the same ]
比對程序輸出的日誌,可以看出,每個ITransientOperation
的輸出都會不一樣;而IScopedOperation
在相同的範圍內輸出內容一樣,在不同的範圍間輸出內容不同;最後一個ISingletonOperation
,每次的輸出都是相同的。這樣的輸出結果,也符合我們前面的表格展示內容。