はじめに
本シリーズは、.NET 8 で Windows Service を作る手順を 段階的に 紹介します。
この記事では「Windows Service として登録できる最小限のコード」を作ります。
開発環境
| 項目 | バージョン |
|---|---|
| .NET | 8.0 (LTS) |
| IDE | Visual Studio 2022 / VS Code |
| OS | Windows 10/11 |
Windows Service とは
Windows が管理するバックグラウンドプロセスです。ユーザーがログインしていなくても起動し続けられる点が通常アプリとの大きな違いです。
| 比較 | 通常のアプリ | Windows Service |
|---|---|---|
| 起動 | 手動 | OS 起動時に自動起動可能 |
| ログイン不要 | 不可 | 可能 |
| UI | あり | なし(バックグラウンド) |
| 管理ツール | タスクマネージャー | services.msc / sc.exe |
プロジェクト構成
Project1_MinimalService/
├── Project1_MinimalService.csproj
├── Program.cs ← エントリポイント
├── Worker.cs ← サービスのメインロジック
├── Services/
│ └── FileLogService.cs ← ファイルへのログ書き込み
└── scripts/
├── install-service.bat ← サービス登録スクリプト
└── uninstall-service.bat ← サービス削除スクリプト
実装
1. プロジェクト作成
CLI またはVisual Studio の「ワーカー サービス」テンプレートで作成します。
dotnet new worker -n Project1_MinimalService --framework net8.0
2. csproj の設定
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Project1_MinimalService</RootNamespace>
<AssemblyName>WindowsServiceDemo_1</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
</Project>
AssemblyName を指定することで、生成される exe のファイル名をプロジェクト名と分けられます。
3. ログ書き込みサービス(FileLogService.cs)
using System.Text;
namespace Project1_MinimalService.Services;
public class FileLogService
{
private readonly string _logFilePath;
private readonly object _lock = new();
public FileLogService()
{
var logDir = Path.Combine(AppContext.BaseDirectory, "logs");
if (!Directory.Exists(logDir))
Directory.CreateDirectory(logDir);
// 日付単位でファイルを作成(同日は追記)
_logFilePath = Path.Combine(logDir, $"service_{DateTime.Now:yyyyMMdd}.log");
}
public void WriteLog(string eventType, string message)
{
var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{eventType}] {message}";
lock (_lock)
{
File.AppendAllText(_logFilePath, line + Environment.NewLine, Encoding.UTF8);
}
}
public string LogFilePath => _logFilePath;
}
マルチスレッドからの同時書き込みに備えて lock を使っています。
4. メインワーカー(Worker.cs)
using Project1_MinimalService.Services;
namespace Project1_MinimalService;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly FileLogService _fileLogService;
public Worker(ILogger<Worker> logger, FileLogService fileLogService)
{
_logger = logger;
_fileLogService = fileLogService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// 起動時ログ
LogEvent("STARTUP", "Windows Service が起動しました。");
try
{
// サービス停止まで待機(最小構成なので何もしない)
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException)
{
// 停止要求は正常終了
}
finally
{
// 停止時ログ
LogEvent("SHUTDOWN", "Windows Service が停止しました。");
}
}
private void LogEvent(string eventType, string message)
{
_fileLogService.WriteLog(eventType, message);
_logger.LogInformation("[{EventType}] {Message}", eventType, message);
}
}
Task.Delay(Timeout.Infinite, stoppingToken) はサービス停止シグナルが来るまで待ち続けるシンプルな実装です。
5. エントリポイント(Program.cs)
using Project1_MinimalService;
using Project1_MinimalService.Services;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "WindowsServiceDemo_1";
});
builder.Services.AddSingleton<FileLogService>();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
AddWindowsService() を呼ぶだけで、コンソール実行(開発中)と Windows Service(本番)の両方に対応できます。
サービスの登録・起動
ビルド(発行)
cd C:\Program Files\Demo\Project1_MinimalService
dotnet publish -c Release -r win-x64 --self-contained -o publish --ignore-failed-sources
--self-contained を指定すると、実行先に .NET ランタイムがなくても動きます。
サービス登録スクリプト(install-service.bat)
@echo off
setlocal
set SERVICE_NAME=WindowsServiceDemo_1
set SERVICE_DISPLAY_NAME=Windows Service Demo 1 - Minimal
set EXE_PATH=%~dp0..\publish\WindowsServiceDemo_1.exe
for %%i in ("%EXE_PATH%") do set EXE_PATH=%%~fi
if not exist "%EXE_PATH%" (
echo [ERROR] Executable not found: %EXE_PATH%
pause & exit /b 1
)
sc query %SERVICE_NAME% >nul 2>&1
if %errorlevel% equ 0 (
sc stop %SERVICE_NAME% >nul 2>&1
timeout /t 3 /nobreak >nul
sc delete %SERVICE_NAME%
timeout /t 2 /nobreak >nul
)
sc create %SERVICE_NAME% ^
binPath= "%EXE_PATH%" ^
DisplayName= "%SERVICE_DISPLAY_NAME%" ^
start= auto
sc start %SERVICE_NAME%
pause
endlocal
binPath=の=の後ろにスペースが必要です(sc.exe の仕様)。
管理者権限のコマンドプロンプトで実行します。
SERVICE_DISPLAY_NAMEがしっかり表示されて実行中になっていますね!
確認・削除
sc query WindowsServiceDemo_1 REM 状態確認
sc stop WindowsServiceDemo_1 REM 停止
sc delete WindowsServiceDemo_1 REM 削除
動作確認
| 確認内容 | 手順 | 期待ログ |
|---|---|---|
| 起動ログ | サービス起動直後にログ確認 | [STARTUP] |
| 停止ログ |
sc stop 後にログ確認 |
[SHUTDOWN] |
ログ出力例:
[2026-06-03 09:00:00] [STARTUP] Windows Service が起動しました。
[2026-06-03 09:30:00] [SHUTDOWN] Windows Service が停止しました。
ログファイルは publish\logs\service_yyyyMMdd.log に出力されます。
ポイントまとめ
-
AddWindowsService()+BackgroundServiceで最小構成のサービスが作れる -
AssemblyNameで exe のファイル名を自由に設定できる -
--self-containedでランタイム不要の自己完結型 exe をビルドできる - 停止シグナルは
CancellationTokenで受け取る
次の記事(#2)では、ロック・スリープ復帰を検知する実装を追加します。
参考
本記事に掲載している内容は、私個人の見解であり、所属する組織の立場や戦略、意見を代表するものではありません。
