以前在 .NET Framework 要寫 Windows 服務在測試時有點麻煩,測試時的 console 和正式的服務兩個要拆開來寫。
現在從 .NET Core 開始有了 Worker Service ,產生的檔案就是 exe ,可以直接做測試,要將 exe 檔案部屬為 Windows 服務只要一行指令。
如果使用 Rider 的話可以直接在左側點選 Worker Service
因為我們的目標是建立 windows 服務,需要使用 NuGet 安裝 WindowsServices ,或是使用下面的 .NET CLI 指令:
在 Program.cs 上面需要增加
使用 NuGet 安裝 Serilog.AspNetCore 套件或是使用 .NET CLI 下指令安裝
註:本篇主要是在說明 Worker Service 專案,所以使用最簡易的步驟寫 log,如果想要查看使用 Serilog 寫 log,可以查看這兩篇:
C# 使用 Serilog 紀錄 Log (不用設定檔)
最詳細 ASP.NET Core 使用 Serilog 套件寫 log 教學
在 Program.cs 上面需要增加下面的程式碼,這樣 log 會輸出在 log 資料夾下面的 log.log 檔案內
但是測試時程式啟動後預設的路徑卻是:
這樣會讓我們輸出的 log 位置不正確,需要在程式一開始執行時就將預設目錄設定為程式執行檔的那個目錄
在 Program.cs 檔案的 new LoggerConfiguration 的上方加上:
建立服務
註:需要使用絕對路徑,不然自動啟動時會找不到執行檔
啟動服務
設定自動啟動服務
停止服務
刪除服務
參考資料:
Worker Services in .NET
建立專案
在 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
留言
張貼留言
如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com