星期五, 12月 08, 2023

[.NET] 在 BackgroundService 內使用 EFCore

延續 [.NET] 使用 BackgroundService 建立 Windows Service 筆記,該文章內的 JokeService 隨機抓取資料顯示行為改用 EFCore 讀取資料庫來取代,EF Core 安裝和連線字串調整請參考該筆記 - [EFCore] 在 Console 專案上安裝設定

因為 AddHostedService 和 DbContext 生命週期因素,一併把 .NET DI 知識補齊,參考文章如下
理解 DI 生命週期後就知道為什麼要透過 IServiceProvider 來存取 DbContext 囉

JobService

透過 IServiceProvider 來抓取 DbContext,而非直接把 DbContext DI 進 JokeService 內
using EFCoreWorkerSample.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreWorkerSample
{
    public class JokeService
    {
        private readonly IServiceProvider _serviceProvider;

        public JokeService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public string GetJoke()
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<EfcoreDbContext>();

            // 從 DB 隨機抓取一個 Joke 出來顯示
            string tsql = @"
                    SELECT TOP 1 *
                    FROM Jokes
                    ORDER BY NEWID()
                    ";

            var joke = dbContext.Jokes.FromSqlRaw(tsql).Single();

            return $@"
                輸出時間:{DateTime.Now}
                問題:{joke.Q}
                答案:{joke.A}";
        }

    }
}

改寫 Worker 內容

namespace EFCoreWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly JokeService _jokeService;

        public Worker(ILogger<Worker> logger, JokeService jokeService)
        {
            _logger = logger;
            _jokeService = jokeService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string message = _jokeService.GetJoke();

                _logger.LogInformation(message);

                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Program.cs 

Service 擴充方法預設生命週期
  • AddHostedService:singletion
  • AddDbContext:scoped
using EFCoreWorkerSample.Models;

namespace EFCoreWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);

            // 使用 AddHostedService 來註冊實作 IHostedService 類別,其生命週期為 Singleton
            builder.Services.AddHostedService<Worker>();

            builder.Services.AddScoped<EfcoreDbContext>();

            builder.Services.AddSingleton<JokeService>();

            var host = builder.Build();
            host.Run();
        }
    }
}

執行結果

DI 生命週期驗證

因為 AddHostedService 註冊 Worker 生命週期為 singletion,所以 JokeService DI 進去時也只能使用 singletion,換成其他生命週期 (scoped 或 transient),就會拋出 exception 啦
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: EFCoreWorkerSample.Worker': Cannot consume scoped service 'EFCoreWorkerSample.JokeService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)

沒有留言:

張貼留言