昨天我們聊了DI 與3種注入方式
今天就讓我們來看看 .NetCore 中的DI
.NetCore 的DI容器主要提供3種服務存留期
Sample: 我們建一個.Net6 web api 的範例程式
新增3個Service跟其對應的實作
public interface ISingletonService
{
Guid Id { get; }
}
public interface IScopedService
{
Guid Id { get; }
}
public interface ITransientService
{
Guid Id { get; }
}
public class SingletonService : IDisposable, ISingletonService
{
public Guid Id { get; } = Guid.NewGuid();
public void Dispose()
{
Console.WriteLine(nameof(SingletonService) + " disposed");
}
}
public class ScopedService : IDisposable, IScopedService
{
public Guid Id { get; } = Guid.NewGuid();
public void Dispose()
{
Console.WriteLine(nameof(ScopedService) + " disposed");
}
}
public class TransientService :IDisposable, ITransientService
{
public Guid Id { get; } = Guid.NewGuid();
public void Dispose()
{
Console.WriteLine(nameof(TransientService) + " disposed");
}
}
然後新增一個Service For Demo用
public interface ILifetimeService
{
void PrintId();
}
class LifetimeService : ILifetimeService
{
private readonly ISingletonService _singletonService;
private readonly IScopedService _scopedService;
private readonly ITransientService _transientService;
public LifetimeService(ISingletonService singletonService, IScopedService scopedService, ITransientService transientService)
{
_singletonService = singletonService;
_scopedService = scopedService;
_transientService = transientService;
}
// print 出每個Service的Id
public void PrintId()
{
Console.WriteLine($"singleton: {_singletonService.Id}");
Console.WriteLine($"scoped: {_scopedService.Id}");
Console.WriteLine($"transient: {_transientService.Id}");
}
}
然後在program.cs
中分別以不同的生命週期註冊他們:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Add LifeTime Service
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddTransient<ITransientService, TransientService>();
builder.Services.AddTransient<ILifetimeService, LifetimeService>();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
新增一個Controller LifeTimeController.cs
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[ApiController]
[Route("[controller]")]
public class LifeTimeController : ControllerBase
{
private readonly ILifetimeService _lifetimeServiceA;
private readonly ILifetimeService _lifetimeServiceB;
private static int _counter = 1;
public LifeTimeController(ILifetimeService lifetimeServiceA, ILifetimeService lifetimeServiceB)
{
_lifetimeServiceA = lifetimeServiceA;
_lifetimeServiceB = lifetimeServiceB;
}
// 每次Get 時會print 出每個Service的Id 跟第幾次呼叫
[HttpGet]
public void Get()
{
Console.WriteLine($"------request count: {_counter++}------");
Console.WriteLine($"------ServiceA---------");
_lifetimeServiceA.PrintId();
Console.WriteLine($"------ServiceB---------");
_lifetimeServiceB.PrintId();
}
}
可以看見,在同一次Request中,Scoped的Id 是相同的,但transient的Id並不相同
而不同次Request中取到的Singleton的Id都一樣
每個留存期的物件只能依賴同樣留存期的物件,否則runtime時會報錯,原因後敘。
要注意循環參考,Ex. A依賴B,B依賴C,C又依賴A
.Net Core 中的DI 主要在兩個nuget 套件中
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.DependencyInjection.Abstractions
前者是預設實作,後者是抽象類
微軟在設計.Net Core 的 DI Container 時
將記錄服務註冊與對應實作的註冊表與實際提供服務的Container 分開放在了兩個類別
前者為IServiceCollection
後者為 IServiceProvider
ServiceDescriptor
這個class 是對註冊的服務的描述IServiceProvider
就是透過這個類別去提供Instance
ServiceDescriptor.cs
public class ServiceDescriptor
{
public ServiceDescriptor(
Type serviceType,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType,
ServiceLifetime lifetime)
: this(serviceType, lifetime)
{
//// 忽略實作
}
public ServiceDescriptor(
Type serviceType,
object instance)
: this(serviceType, ServiceLifetime.Singleton)
{
//// 忽略實作
}
public ServiceDescriptor(
Type serviceType,
Func<IServiceProvider, object> factory,
ServiceLifetime lifetime)
: this(serviceType, lifetime)
{
//// 忽略實作
}
private ServiceDescriptor(Type serviceType, ServiceLifetime lifetime)
{
Lifetime = lifetime;
ServiceType = serviceType;
}
public ServiceLifetime Lifetime { get; }
public Type ServiceType { get; }
public Type? ImplementationType { get; }
public object? ImplementationInstance { get; }
public Func<IServiceProvider, object>? ImplementationFactory { get; }
}
其中 ServiceLifeTime
這個類別是個enum,提供了我們前述的生命週期
ServiceLifeTime.cs
public enum ServiceLifetime
{
Singleton,
Scoped,
Transient
}
另外在建構 ServiceDescriptor
主要有幾種方式
如果是用現成的執行個體建構 ServiceDescriptor
則LifeTime 會被設定成Singleton
其餘兩者需要帶入實際的LifeTime
另外此類別還有提供靜態工廠方法去建立 ServiceDescriptor
,可以幫助使建立ServiceDescriptor
更加容易(我就不把完整程式碼貼上來了)
IServiceCollection
的預設實現是 ServiceCollection
本質上就是ServiceDescriptor
的集合
public interface IServiceCollection : IList<ServiceDescriptor>{}
public class ServiceCollection :
IServiceCollection,
ICollection<ServiceDescriptor>,
IEnumerable<ServiceDescriptor>,
IEnumerable,
IList<ServiceDescriptor>{...}
所謂的註冊服務,實際上就是建立對應的ServiceDescriptor
並加到ServiceCollection中
我們在上面介紹服務留存期時在Program.cs
中註冊服務的語法
builder.Services.AddSingleton<ISingletonService, SingletonService>
實際上背後所做的事,其實是建一個
Lifetime 是 Singleton
,
ServiceType為ISingletonService
,
ImplementationType為SingletonService
的ServiceDescriptor
,並將其加到ServiceCollection
中
上面的註冊方式也可以改寫為
builder.Services.Add(new ServiceDescriptor(typeof(ISingletonService),typeof(SingletonService), ServiceLifetime.Singleton));
我們有了一組描敘服務與對應的實作該如何取得的IServiceCollection
了,接著就是從中取出我們實際要用的服務了IServiceProvider
這個提供了唯一的一個方法GetService
來取得物件
IServiceProvider.cs
public interface IServiceProvider{
object? GetService(Type serviceType);
}
我們可以透過IServiceCollection
的 BuildServiceProvider()
方法來取得 ServiceProvider
ServiceProvider
本身實作了 IServiceProvider
這個介面
Sample.cs
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ISingletonService, SingletonService>();
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddTransient<ITransientService, TransientService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var singletonService = serviceProvider.GetService<ISingletonService>();
Console.WriteLine(singletonService.Id);
此外BuildServiceProvider()
有提供多載 BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
ServiceProviderOptions.cs
public class ServiceProviderOptions
{
internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions();
public bool ValidateScopes { get; set; }
public bool ValidateOnBuild { get; set; }
}
ValidateScopes
:執行檢查,確認已設定範圍的服務永遠不會從根提供者取得解析(範圍與根提供者明天會提到)ValidateOnBuild
: 執行檢查,確認每個註冊的ServiceDescriptor
都能提供對應的執行個體
其實DI本來只打算寫2篇,但這幾天工作實在有點忙,只好拆成好幾天寫了(開始擔心30天不夠了XD)。
明天預計會提服務留存期實際上是怎麼做到的
跟實際上的在平常使用建構式注入時框架是如何透過DI提供對應服務的執行個體的