星期五, 12月 01, 2023

[.NET] 使用 BackgroundService 建立 Windows Service

根據官方文章 - Create Windows Service using BackgroundService 的筆記,該內容為使用 BackgroundService 建立 Windows Service,並從 JobService 內隨機取出資料塞進 Windows 事件內

建立背景工作服務 (Worker Service Template) 專案

輸入關鍵字 Worker 就可以找到 [背景工作服務範本]
輸入專案名稱並決定檔案位置
架構為 [.NET 7.0] 和 [不要使用最上層陳述式] 為預設值
nuget 上安裝 Microsoft.Extensions.Hosting.WindowsServices

建立 Job Service

會從該 Service 內隨機取出資料,該內容出處在 github 上,官方文章內只有使用部分內容,筆記只列出 5 個示意而已
namespace BackgroundServiceSample
{
    public sealed class JokeService
    {
        public string GetJoke()
        {
            Joke joke = _jokes.ElementAt(
                Random.Shared.Next(_jokes.Count));

            return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
        }

        private readonly HashSet<Joke> _jokes = new()
        {
            new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
            new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
            new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
            new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
            new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem")
        };
    }

    readonly record struct Joke(string Setup, string Punchline);
}

改寫 Worker 

JobService 會透過 DI 注入至 Worker 內,ExecuteAsync 內則是會隨機抓取資料出來,另外為了觀察生命週期,會覆寫 StartAsync 和 StopAsync,Service 啟動時可以觀察
namespace BackgroundServiceSample
{
    public class Worker : BackgroundService
    {
        private readonly JokeService _jokeService;
        private readonly ILogger<Worker> _logger;

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

        // 覆寫 StartAsync 或 StopAsync 方法時,您必須呼叫和 awaitbase 類別方法,以確保服務正常啟動或關閉。
        public override Task StartAsync(CancellationToken cancellationToken)
        {
             _logger.LogWarning(nameof(StartAsync));

            return base.StartAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    string joke = _jokeService.GetJoke();
                    _logger.LogWarning("{Joke}", joke);

                    await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
                }
            }
            catch (OperationCanceledException)
            {
                // When the stopping token is canceled, for example, a call made from services.msc,
                // we shouldn't exit with a non-zero exit code. In other words, this is expected...
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{Message}", ex.Message);

                // Terminates this process and returns an exit code to the operating system.
                // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
                // performs one of two scenarios:
                // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
                // 2. When set to "StopHost": will cleanly stop the host, and log errors.
                //
                // In order for the Windows Service Management system to leverage configured
                // recovery options, we need to terminate the process with a non-zero exit code.
                Environment.Exit(1);
            }
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogWarning(nameof(StopAsync));

            return base.StopAsync(cancellationToken);
        }
    }
} 
  
下述 EventLog 設定,會在應用程式內紀錄來源為 The Joke Service 的事件資料,可以在 appsettings.{Environment}.json (該筆記情況 Environment 為 Development) 內指定,或是參考該官方文章 -  Logging providers in .NET: Configure Windows EventLog,直接在 Program 內用 C# 設定
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

改寫 Program class

在 Unhandled exceptions from a BackgroundService 提到說,Windows Service 在 ExecuteAsync() 內發生 Exception 時的處理行為,BackgroundServiceExceptionBehavior Enum 有兩個選項
  • Ignore:Ignore exceptions thrown in BackgroundService.
  • StopHost:The IHost will be stopped when an unhandled exception is thrown.
在 .NET 6 以前預設值為 Ignore,.NET 6 開始為 StopHost
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

namespace BackgroundServiceSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            IHost host = Host.CreateDefaultBuilder(args)
                // 建立 Windows Service:.NET Joke Service
                .UseWindowsService(options =>
                {
                    options.ServiceName = ".NET Joke Service";
                })
                .ConfigureServices(services =>
                {
                    // 設定 ExecuteAsync() 內發生 Exception 時的處理行為
                    services.Configure<HostOptions>(hostOptions =>
                    {
                        hostOptions.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.StopHost;
                    });

                    LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(services);

                    services.AddHostedService<Worker>();
                    services.AddSingleton<JokeService>();
                })
                .Build();

            host.Run();
        }
    }
}

發佈

在 Project 上滑鼠右鍵並選擇發佈,目標選擇 [資料夾]
設定資料夾位置,預設都在專案檔案內的 bin 資料夾內
設定檔
  • 部屬模式:獨立式
  • 勾選 [產生單一檔案]
  • 勾選 [啟用 ReadyToRun 編譯]
完成設定檔案後,進行發佈

建立 Windows Service

使用 Windows Service Control Manager's (sc.exe) 來建立 Windows Service,相關語法整理如下
// 建立啟動類型為自動的 Windows Service
sc.exe create ".NET Joke Service" 
    binpath="D:\BackgroundServiceSample\bin\Release\net7.0\publish\win-x64\BackgroundServiceSample.exe"
    start= auto

// 啟用 .NET Joke Service
sc.exe start ".NET Joke Service"

// 停用 .NET Joke Service
sc.exe stop ".NET Joke Service"

// 刪除 .NET Joke Service
// 服務在停止狀態,刪除才會直接移除,服務啟動情況下使用刪除,該服務會呈現已停用狀態,重新開機才會真的移除掉
sc.exe delete ".NET Joke Service"

觀察 Windows Service 執行結果

沒有留言:

張貼留言