iT邦幫忙

2022 iThome 鐵人賽

DAY 4
0
Modern Web

擁抱 .Net Core系列 第 4

[Day4] .Net Core 中的相依性注入 - 1

  • 分享至 

  • xImage
  •  

昨天我們聊了DI 與3種注入方式
今天就讓我們來看看 .NetCore 中的DI

服務存留期(Service lifetimes)

.NetCore 的DI容器主要提供3種服務存留期

  • Transient - 每次請求都從DI容器中取一個新的Instance
  • Scoped - 每個Client 請求從DI容器中取一個新的執行個體,在這個request中所拿到的執行個體都是同一個。
  • Singleton - 當執行個體第一次被使用到或解析時建立,後面所有使用到的執行個體都會視同一個

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();
    }
}

https://ithelp.ithome.com.tw/upload/images/20220915/201095493nhL6mx8vl.png

可以看見,在同一次Request中,Scoped的Id 是相同的,但transient的Id並不相同
而不同次Request中取到的Singleton的Id都一樣

每個留存期的物件只能依賴同樣留存期的物件,否則runtime時會報錯,原因後敘。
要注意循環參考,Ex. A依賴B,B依賴C,C又依賴A

DI Container

.Net Core 中的DI 主要在兩個nuget 套件中

  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.DependencyInjection.Abstractions

前者是預設實作,後者是抽象類
微軟在設計.Net Core 的 DI Container 時
將記錄服務註冊與對應實作的註冊表與實際提供服務的Container 分開放在了兩個類別
前者為IServiceCollection 後者為 IServiceProvider

ServiceDescriptor

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 主要有幾種方式

  • 指定實際實作的類型 (ImplementationType)
  • 一個現成的執行個體 (ImplementationInstance)
  • 一個負責提供執行個體的工廠 (ImplementationFactory)

如果是用現成的執行個體建構 ServiceDescriptor 則LifeTime 會被設定成Singleton
其餘兩者需要帶入實際的LifeTime

另外此類別還有提供靜態工廠方法去建立 ServiceDescriptor ,可以幫助使建立ServiceDescriptor更加容易(我就不把完整程式碼貼上來了)

IServiceCollection

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));

IServiceProvider

我們有了一組描敘服務與對應的實作該如何取得的IServiceCollection 了,接著就是從中取出我們實際要用的服務了
IServiceProvider 這個提供了唯一的一個方法GetService 來取得物件

IServiceProvider.cs

public interface IServiceProvider{
    object? GetService(Type serviceType);
}

我們可以透過IServiceCollectionBuildServiceProvider()方法來取得 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提供對應服務的執行個體的


上一篇
[Day3] 工廠與相依性注入,談談IoC 與DI - 2
下一篇
[Day5] .Net Core 中的相依性注入 - 2
系列文
擁抱 .Net Core30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言