.NET 8でのWindowsサービスの作り方

## 概要

.NET(Core)になってから、Windowsサービスの作り方は.NET Frameworkの時とはだいぶ違うものになりました。ASP.NETをベースにしたGenericHostを使用するのが基本になりましたし、インストール方法も.NET Framework以前のものに戻っています。

Windowsサービス関連の話題にも何度か触れていますが、意外にサービス自体の作り方をまとめていなかったので、ここで書いてみようと思います。

## 最初にまとめ

大雑把に言えば、テンプレート「ワーカーサービス」を選んで、NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加して、GenericHostの初期化処理にAddWindowsService()の呼び出しを追加。これだけです。

あとは必要に応じてイベントハンドラやプロパティを設定します。Windowsサービスのライフサイクルに対応させる場合は、WindowsServiceLifeTimeを継承したクラスをさらに追加します。

これを、コードサンプルを添えながら説明していきます。説明に使ったコードは[GitHubにも置いておきます](https://github.com/suusanex/sample_windows_service_dotnet8/)。

## 最小限のWindowsサービス

次の手順で、Windowsサービスとしてインストールできる最低限のものが作れます。

1. テンプレート「ワーカー サービス」を選んでプロジェクト作成
2. NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加
3. AddWindowsServiceでサービス追加
4. Worker.ExecuteAsync()を抜けるようにする（抜けるとサービスのStartが完了したことになる）
5. 発行したら、「sc create サービス名 binPath=”～.exe”」でサービスインストール。「sc start サービス名」でサービス開始

テンプレートとNuGetは省略して説明します。

### AddWindowsServiceでサービス追加

.NET 8のテンプレートならば、Program.csに次のようなコードが有ると思います。

```csharp
var builder = Host.CreateApplicationBuilder(args);
var host = builder.Build();
host.Run();
```

GenericHostは、ここに必要な物を足していく使い方になります。

Windowsサービスにしたい場合、次のようにAddWindowsServiceを追加して、パラメータでサービス名を設定します。

```csharp
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "sample-windows-service-dotnet8";
});
```

### Worker.ExecuteAsync()を抜けるようにする

ワーカーサービスのテンプレートは、起動するとWorkerクラスのExecuteAsyncメソッドが呼ばれ、そこで無限ループするように作られています。コンソールアプリとしてそのまま起動する場合、ExecuteAsyncを抜けたらプロセスが終了します。

しかし、Windowsサービスとして起動する場合は、「サービス開始時にExecuteAsyncが呼ばれ、それを抜けるとサービスがRunning状態になる」という動きに変わります。そのため、ExecuteAsyncの無限ループをやめる必要があります。例えばこんな感じです。

```csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
    await Task.CompletedTask;
}
```

### 発行

サービスとしてインストールするために、発行をします。これはWindowsサービスでなくても同じなので、この記事では説明しません。

### サービスインストール・開始

サービスインストールは、.NET Frameworkでは専用のインストール方法が必要でした。しかし.NET(Core)では、ネイティブC++のサービスと同様にsc.exeでインストールする方式になっています。

インストールはsc createです。色々なパラメータ指定は[コマンド仕様](https://learn.microsoft.com/ja-jp/windows-server/administration/windows-commands/sc-create)を見た方が速いですが、最低限のインストールであれば、サービス名とexeのフルパスを指定すればOKです。

```csharp
sc create "（サービス名）" binPath="（サービスのフルパス）.exe"

例：
sc create "sample-windows-service-dotnet8" binPath="C:\Service\sample-windows-service-dotnet8.exe"
```

開始はsc startです。

```csharp
sc start "（サービス名）"

例：
sc start "sample-windows-service-dotnet8"

```

### 動作確認

ワーカーサービス（というよりGenericHost）はデフォルトでは、コンソールアプリの標準出力へログを出す設定になっています。しかし、サービスとして動かす場合はこのログが見れません。

サービスの動作ログを手軽に見るには、AddDebugでデバッグメッセージへの出力を追加したり、AddEventLogでイベントログへの出力を追加すると便利です。これはGenericHostのILoggerの使い方の説明となるため、本記事ではこれ以上は説明しません。

```csharp
var builder = Host.CreateApplicationBuilder(args);
（略）
builder.Logging.AddDebug();
builder.Logging.AddEventLog();
```

## Windowsサービスのライフサイクルを追加

最低限のままだと、WindowsサービスのStart・Stopといったライフサイクルに対応できていません。Worker(BackgrondService)から、Windowsサービスのライフサイクルへ置き換えてみます。

### ライフサイクルを実装するクラスを追加

WindowsServiceLifeTimeを継承したクラスを作成します。最低限の実装はコンストラクタだけです。次のように、最低でもILoggerは入れておくと便利だと思います。

```csharp

    internal class SampleServiceLifetime : WindowsServiceLifetime
    {
        public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
        {
            _logger = logger;
        }

        public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
        {
	          _logger = logger;
        }
        private readonly ILogger<SampleServiceLifetime> _logger;
    }    
```

このクラスを、IHostLifetimeをキーにしてシングルトンでDIコンテナに登録します。デバッグ実行の時に呼ばれないように、IsWindowsServiceで判定して、サービス起動した時だけ登録するようにするとより良いです。（DIはWindowsサービスと直接関係ないので説明省略します）

```csharp
var builder = Host.CreateApplicationBuilder(args);
（略）
if (WindowsServiceHelpers.IsWindowsService())
{
    builder.Services.AddSingleton<IHostLifetime, SampleServiceLifetime>();
}
```

### ライフサイクルのイベントの実装

WindowsServiceLifetimeを継承したクラスに戻り、必要なメソッドをオーバーライドします。例えばStartのイベントの処理をしたい場合は、次のようにOnStartをオーバーライドします。（動作確認用にログを入れています）

```csharp
internal class SampleServiceLifetime : WindowsServiceLifetime
{
    protected override void OnStart(string[] args)
    {
        _logger.LogInformation("OnStart");
        base.OnStart(args);
    }
}
```

### ライフサイクルのパラメータの設定

WindowsServiceLifetimeを継承したクラスに戻り、必要なプロパティをコンストラクタで設定します。例えばStopを禁止したい場合はこんな感じになります。

```csharp

internal class SampleServiceLifetime : WindowsServiceLifetime
{
    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    private void CommonSetParams()
    {
        CanStop = false;
    }

}
```

### ちなみにユーザーセッション情報を取る場合は

ユーザーセッション情報を取りたい場合はOnSessionChangeをオーバーライドすることで、SessionLogonなどの情報が撮れます。ただし、CanHandleSessionChangeEventプロパティをtrueにしないとイベントが来ないので注意です。

```csharp
internal class SampleServiceLifetime : WindowsServiceLifetime
{
    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    private void CommonSetParams()
    {
        CanHandleSessionChangeEvent = true;
    }

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    {
        base.OnSessionChange(changeDescription);
    }
 }
```

# まとめ

Windowsサービスの作り方はほとんどC#のコードで完結できてだいぶ楽になったものの、昔の書き方とはだいぶ違うので知らないとだいぶ戸惑いそうです。この記事にまとめたような基本的なポイントを抑えておくと、いざ新しく作ろうとした時にスムーズに行くと思います。