只有累積,沒有奇蹟

2019年4月6日 星期六

[.NETCore] ASP.NET Core DI 生命週期 LifeTime

前言
最近在 Code Review 時主管提到關於 ASP.NET Core 的一些基本觀念,發現自己對於 .Core DI Service lifetime 部分有些模糊,在 .NET Core 中整個框架建立在 DI ( Dependency Injection) 之上,到處都可以看到 DI 的影子,因此決定寫篇文章釐清自己的觀念,若有問題或是有更推薦的方法歡迎提出一起討論或是給予指導。

.NET Core DI
.NET Core 有提供內建 DI Framework,命名空間為  Microsoft.Extensions.DependencyInjection 使用 DI 容器服務第一步需要先做註冊,在 .NET Core DI 是在  startup.cs  中的 ConfigureServices 使用  IServiceCollection  的擴充方法指定服務容器生命週期 (LifeTime),預設提供的擴充方法與說明為
  • Transient : 每次請求時都會產生新的 Instance
  • Scoped : 每個 http Request 都會產生一份 Instance
  • Singleton : 整個 Application 只會有一份 Instance
看完生命週期的說明是不是覺得很抽象,如果第一次看就看懂的話實在是太厲害了,為了說明上面三種生命週期的差異性,以下舉個簡單的範例讓大家更容易理解

首先,先建立.NET Core Console 專案,專案建立完畢後新增一個 interface 為 IOperation,其屬性 OperationId 為 guid 型別為唯一識別碼,以及其他不同生命週期的 interface 分別實作基本 IOperation 介面
public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation{}

public interface IOperationScoped : IOperation{}

public interface IOperationSingleton : IOperation{}

public interface IOperationSingletonInstance : IOperation{}
接著新增 Operation 類別並實作不同生命週期的介面,並定義如果未提供 guid 時自己產生一份新的 guid 
public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}
接下來我們在 program.cs main 方法新增 ServiceCollection instance 並註冊 IOperationScoped、IOperationTransient、IOperationSingleton 到對應的 AddScoped、AddTransient、AddSingleton 生命週期中,接著透過 service.BuildServiceProvider 方法新增 scope
using Microsoft.Extensions.DependencyInjection;
using System;

namespace EFCoreConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var services = new ServiceCollection()
            .AddScoped<IOperationScoped, Operation>()
            .AddTransient<IOperationTransient, Operation>()
            .AddSingleton<IOperationSingleton, Operation>();

            var provider = services.BuildServiceProvider();
            using (var scope1 = provider.CreateScope())
            {
                var p = scope1.ServiceProvider;
                Console.WriteLine("<SCOPE 1>");
                Console.WriteLine("---Test 1---");
                Console.WriteLine($"transient1: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope1    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton1: {p.GetService<IOperationSingleton>().OperationId}");
                Console.WriteLine(" ");
                Console.WriteLine("---Test 2---");
                Console.WriteLine($"transient2: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope2    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton2: {p.GetService<IOperationSingleton>().OperationId}");
                Console.WriteLine(" ");
            }

            using (var scope2 = provider.CreateScope())
            {
                var p = scope2.ServiceProvider;
                Console.WriteLine("<SCOPE 2>");
                Console.WriteLine("---Test 3---");
                Console.WriteLine($"transient3: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope3    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton3: {p.GetService<IOperationSingleton>().OperationId}");
            }

            Console.ReadKey();
        }
    }
}

輸出如下
<SCOPE 1>
---Test 1---
transient1: ee48347a-a819-402b-8e4b-2d0e6a480186
scope1    : b7d9f907-6f95-4cbb-8ccb-3a2fb247287a
singleton1: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779

---Test 2---
transient2: 03a58729-7efe-4394-a9ae-0197e4a14e5c
scope2    : b7d9f907-6f95-4cbb-8ccb-3a2fb247287a
singleton2: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779

<SCOPE 2>
---Test 3---
transient3: 7c7334af-62e4-4085-b7a5-9876865412c1
scope3    : fe99001b-3566-4850-bc17-6757e7051eeb
singleton3: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779
透過以上測試與觀察輸出結果,可以得到以下結論
  • Transient : 每次請求時都會產生新的 Instance > 每次都不一樣
  • Scoped : 每個 http Request 都會產生一份 Instance > 相同 Scoped 時會相同,不同 Scoped 時會不一樣
  • Singleton : 整個 Application 只會有一份 Instance > 整個應用程式一份
示意圖

使用情境
簡單來說,可以依據情境做不同的生命週期註冊的動作,舉例當 EF Core 中的 DBContext 當未設定生命週期時預設就會是 Scoped 使用,可以跨 Service 不用重複 new instance;如果今天是與 Redis 建立單一連線情境,避免建立過多連線造成 Timeout,這時候就是用到 Singletion 設定;如果是每次 request 都是獨一的就可以使用 transient,當你工作上有需要都可以思考看看哪種生命週期最適合。

參考
.NET Core 中的相依性插入
全面理解 ASP.NET Core 依赖注入

0 意見:

張貼留言

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com