最詳細 C# 使用 .NET 6 的 Worker Service 專案快速建立 Windows 服務教學

以前在 .NET Framework 要寫 Windows 服務在測試時有點麻煩,測試時的 console 和正式的服務兩個要拆開來寫。 現在從 .NET Core 開始有了 Worker Service ,產生的檔案就是 exe ,可以直接做測試,要將 exe 檔案部屬為 Windows 服務只要一行指令。

建立專案

在 Visual Studio 中搜尋 Worker Service 建立專案

如果使用 Rider 的話可以直接在左側點選 Worker Service

因為我們的目標是建立 windows 服務,需要使用 NuGet 安裝 WindowsServices ,或是使用下面的 .NET CLI 指令:
	
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
    

在 Program.cs 上面需要增加
	
using WorkerServiceTest;

IHost host = Host.CreateDefaultBuilder(args)
    .UseWindowsService(options => { options.ServiceName = "WorkerServiceTest"; })
    .ConfigureServices(services => { services.AddHostedService<Worker>(); })
    .Build();

await host.RunAsync();
    

伺服器垃圾回收

在 Worker Service 專案預設是不會開啟伺服器垃圾回收 (GC),如果要啟用的話可以在 csproj 檔案裡面加入下面這行:
	
<Project Sdk="Microsoft.NET.Sdk.Worker">
    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UserSecretsId>dotnet-WorkerServiceTest</UserSecretsId>
        <ServerGarbageCollection>true</ServerGarbageCollection>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
        <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
    </ItemGroup>
</Project>

    

log 設定

要將 log 寫到檔案中最簡單的方式就是使用第三方套件,我們使用 Serilog 套件做示範
使用 NuGet 安裝 Serilog.AspNetCore 套件或是使用 .NET CLI 下指令安裝
	
dotnet add package Serilog.AspNetCore
    

註:本篇主要是在說明 Worker Service 專案,所以使用最簡易的步驟寫 log,如果想要查看使用 Serilog 寫 log,可以查看這兩篇:
C# 使用 Serilog 紀錄 Log (不用設定檔)
最詳細 ASP.NET Core 使用 Serilog 套件寫 log 教學

在 Program.cs 上面需要增加下面的程式碼,這樣 log 會輸出在 log 資料夾下面的 log.log 檔案內
	
using Serilog;
using WorkerServiceTest;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()
    .WriteTo.File("logs/log.log")
    .CreateLogger();

IHost host = Host.CreateDefaultBuilder(args)
    .UseSerilog()
    .UseWindowsService(options => { options.ServiceName = "WorkerServiceTest"; })
    .ConfigureServices(services => { services.AddHostedService<Worker>(); })
    .Build();

await host.RunAsync();
    

修改程式預設目錄

我們在測試時的執行檔案是在:
    
WorkerServiceTest\WorkerServiceTest\bin\Debug\net6.0\WorkerServiceTest.exe
    

但是測試時程式啟動後預設的路徑卻是:
    
WorkerServiceTest\WorkerServiceTest
    

這樣會讓我們輸出的 log 位置不正確,需要在程式一開始執行時就將預設目錄設定為程式執行檔的那個目錄

在 Program.cs 檔案的 new LoggerConfiguration 的上方加上:
	
Environment.CurrentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    

撰寫服務程式內容

我們來改寫 Worker.cs ,今天要示範的功能是監視指定資料夾,將資料夾內的檔案複製到指定資料夾內。
	
namespace WorkerServiceTest;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IConfiguration _config;

    public Worker(ILogger<Worker> logger, IConfiguration config)
    {
        _logger = logger;
        _config = config;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string sourcePath = _config["SourcePath"];
        string targetPath = _config["TargetPath"];

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);

            if (!Directory.Exists(sourcePath))
            {
                _logger.LogError(
                    "Source path does not exist, please check the configuration, source path: {SourcePath}",
                    sourcePath);
            }

            if (!Directory.Exists(targetPath))
            {
                _logger.LogError(
                    "Destination path does not exist, please check the configuration, destination path: {DestinationPath}",
                    targetPath);
            }

            CopyFiles(sourcePath, targetPath);

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

    /// <summary>
    /// 將來源資料夾下的所有檔案複製到目的資料夾內
    /// </summary>
    private void CopyFiles(string sourcePath, string targetPath)
    {
        foreach (var file in Directory.GetFiles(sourcePath))
        {
            var sourceFile = Path.GetFileName(file);
            var targetFile = Path.Combine(targetPath, sourceFile);

            if (File.Exists(targetFile) &&
                new FileInfo(file).Length == new FileInfo(targetFile).Length &&
                File.GetLastWriteTime(file) == File.GetLastWriteTime(targetFile))
            {
                _logger.LogDebug("File {SourceFile} already exists in target directory, skipping", sourceFile);
                continue;
            }

            try
            {
                File.Copy(file, targetFile, true);
                _logger.LogInformation("Copied file {SourceFile} to {TargetFile}", sourceFile, targetFile);
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Error copying file {SourceFile} to {TargetFile}", sourceFile, targetFile);
            }
        }
    }
}
    

部屬服務

註:以下指令都需要系統管理員權限才能正常執行

建立服務
註:需要使用絕對路徑,不然自動啟動時會找不到執行檔
    
sc.exe create WorkerServiceTest binpath="WorkerServiceTest\WorkerServiceTest\bin\Debug\net6.0\WorkerServiceTest.exe"
    

啟動服務
    
net start WorkerServiceTest


設定自動啟動服務
    
sc.exe config WorkerServiceTest start=auto


停止服務
    
net stop WorkerServiceTest


刪除服務
    
sc.exe delete WorkerServiceTest



參考資料:
Worker Services in .NET

留言