メインコンテンツまでスキップ

コンソールアプリからステップアップ!テンプレートから学ぶ .NET Worker Service

鶴田
ディアシステム(株)開発一部第2課

こんにちは。ディアシステム開発1部開発2課の鶴田です。

はじめに

みなさんは、昔から動いているコンソールアプリを「そろそろサービス化したいな」と思ったことはありませんか? 私自身そんな場面に出会い、調べてみると 「Worker Service」 という仕組みが .NET では一般的だと知りました。

せっかくなので勉強してみたところ、「これならコンソールアプリの延長線上で、ちゃんとした常駐サービスが作れるじゃないか!」と感じました。

この記事では、その時の学びをまとめつつ、最小限のサンプルコード(HelloWorld!)をもとに紹介していきます。 読んでいただいた方にとってコンソールアプリからのステップアップのきっかけになればうれしいです。

コンソールアプリと Worker Service の違い

実務で「ずっと動かしておきたい処理」をコンソールアプリで作ると、ちょっと不便が出てきます。

  • 自分で「終了条件」や「再起動」の仕組みを用意しないといけない
  • Windows ならサービスとして登録するのが面倒
  • Linux なら systemd などと組み合わせる必要がある

そこで登場するのが Worker Service です。 Worker Service は、コンソールアプリと似た書き心地なのに「常駐サービスとして動かす」ことに特化しています。しかもライフサイクル管理(開始・停止)、ログ出力、DI(依存性注入)まで標準で備わっているので、実務で安心して使えます。

コンソールアプリと Worker Service を比較し表にしました。

項目コンソールアプリWorker Service
終了管理自前で実装が必要OSからの停止通知を受け取れる
適した用途ツール、簡単なバッチ処理ファイル監視や定期処理などの常駐処理

お手軽なコンソールアプリと、実務面で強化された Worker Service といった感じですね。

Worker Service の最小サンプル

「Worker Serviceってどうやって書くの?」と気になりますよね。今回は標準的なテンプレートを作成、内容を細かく確認し、 HelloWorld してみましょう(笑)

まずはテンプレートを作成します。作成するには、.NET SDK が必要ですのであらかじめインストールしておいてください。

コマンドプロンプトで

dotnet new worker -n sampleworker

と入力します。なんやかんやと出てきていろいろ作ってくれます。 このdotnetコマンドは .NET SDK に含まれる機能で、テンプレートを作成してくれるコマンドです。今回は worker サービスと呼ばれる「常駐サービスの基本形」をつくってもらいました。

作成されたフォルダを VSCode で開いて細部を確認してみましょう!2つのcsファイルが作成されているはずです。

Program.cs

using sampleworker;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

Worker.cs

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

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}

では1つずつ解説です。

Program.csの解説

Program.cs、最初の1行目より!

var builder = Host.CreateApplicationBuilder(args);

これ、.NET 6 以降の定番の書き方らしいです。えらく短いです。

Host.CreateApplicationBuilder ですが、.NET 6 以降で導入されたそうです。 実行環境を構成するためビルダーと呼ばれるインスタンスオブジェクトを作成します。

このビルダーを使うと

  • サービスの登録
  • アプリの設定ファイル
  • ログ出力機能の構成

など、たいていの Worker で必要な設定をまとめて行ってくれるそうです。便利です!

あともう一つ。args ですが、これは昔の書き方にすると・・・

public int main(string[] args)
{
}

と記述されていた時の args です。いわゆるコマンドライン引数ですね。 このコマンドラインは設定や構成(例:DB接続情報、RestAPI接続情報など)に使うことが一般的なようです。

つぎです。

builder.Services.AddHostedService<Worker>();

CreateApplicationBuilderコマンドで作成された builder には、依存性注入(DI)用の領域があります。これがDIコンテナーサービスです。この Services プロパティを通じて、アプリケーション全体で利用できるサービス(クラスやインターフェース)を追加・設定できます。

当テンプレートでは Worker クラスをバックグラウンドサービスとして登録しているのが分かります。 ちなみにですが、AddHostedService メソッド以外にも様々なメソッドが用意されており、用途によって使い分けることができます。

メソッド名用途
AddSingleton<TService, TImplementation>() アプリケーション全体で1つだけインスタンスを生成し、使い回します。
AddScoped<TService, TImplementation>() スコープ(通常は1リクエスト)ごとにインスタンスを生成します。
AddTransient<TService, TImplementation>() サービスが要求されるたびに新しいインスタンスを生成します。
AddDbContext<TContext>() Entity Framework Core のDbContextを登録します。
AddLogging() ロギング機能を追加します。
AddHttpClient() HTTP通信用の HttpClient をDIコンテナに登録します。

つぎです。

var host = builder.Build();

builder へ設定した、アプリケーション全体で利用できるサービス(クラスやインターフェース)をもとに、ホストオブジェクトを生成する処理です。Build() メソッドを呼び出すと、サービスに登録した仕組みが初期化され、利用可能になります。

つぎです。

host.Run();

host.Run() を呼び出すことで、アプリケーションが起動します。 起動後は登録済みのサービスは変更できなくなります。

ここまでが Program.cs の説明です。

次から Worker.cs の説明です。同様に1行ずつ確認していきます。

Worker.csの解説

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

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}

では最初の1行めからです。

public class Worker : BackgroundService

BackgroundService を継承しています。 BackgroundService とはどのようなクラスでしょうか

Microsoft Learn BackgroundService クラス

Learn を確認すると、最初に実行されるのは ExecuteAsync( CancellationToken ) のようですが・・・CancellationTokenとはなんだ??(ΦωΦ)

Microsoft Learn CancellationToken 構造体

この解説によると、操作を取り消す通知を配信してくれるそうです。

取り消しが要求されたかどうかは、プロパティ IsCancellationRequested を確認するとよい、と。この機能を使って、

  • 取り消しが要求されるまで、一定時間ごとに処理を繰り返す。例: while (!stoppingToken.IsCancellationRequested)

機能を実現できるというわけですね。BackgroundServiceについては後で詳細に確認します。

つぎです。

    public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

Workerクラスのコンストラクタです。引数として ILogger<Worker> を指定しています。しかし、Program.cs にて ILogger<Worker> は一切指定していません。ILogger<Worker> とは何者なのでしょうか。ソースを確認してみましょう。VSCode上で選択し、F12でソースを確認できます。

interface ILogger<out TCategoryName> の解説

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Logging
{
/// <summary>
/// A generic interface for logging where the category name is derived from the specified
/// <typeparamref name="TCategoryName"/> type name.
/// Generally used to enable activation of a named <see cref="ILogger"/> from dependency injection.
/// </summary>
/// <typeparam name="TCategoryName">The type whose name is used for the logger category name.</typeparam>
public interface ILogger<out TCategoryName> : ILogger
{

}
}

コメントをAIに翻訳してもらいましょう

【AIによる翻訳ここから】

指定された 型名からカテゴリ名が導出される、ロギング用のジェネリックインターフェース。 通常、依存性注入から名前付き を有効化するために使用される。 ロガーのカテゴリ名として使用される型。

【AIによる翻訳ここまで】

名前空間と合わせて考えると、 Microsoft が用意した標準的なロギング用のインターフェースのようです。

続けてみていきましょう。ほかにも不思議な箇所があります。インターフェース ILogger を インターフェース ILogger<out TCategoryName> で再定義していますね・・・(´・ω・`)ナンデヤロ? 目的を確認してみましょう。

  • 依存性注入(DI)で ILogger<Worker> のように型を指定するだけで、そのクラス専用のロガーが自動で注入される
  • クラスごとに自動でカテゴリ名(通常はクラス名)が割り当てられ、ログ出力時に「どのクラスから出たログか」が明確になる
  • ログのフィルタリングや出力先の切り替えなどを、カテゴリ単位で柔軟に制御できる

なるほど・・・カスタマイズ済みのロガーを自動で注入できるようになり、ログの出力元が分かるようになり、さらにログの使い分けもできる、と。かなり柔軟でええ感じにできそうです。これでテックブログ1本かけそう!

横道に逸れそうなので深堀りはしないでおきます。

つぎぃ!

abstract class BackgroundService の解説

protected override async Task ExecuteAsync(CancellationToken stoppingToken)

override しているのは・・・そーいえば BackgroundService を継承していましたね。継承元の BackgroundService は、 Worker Service の基本動作を「共通化」するクラスです。BackgroundService を確認してみましょう。

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task? _executeTask;
private CancellationTokenSource? _stoppingCts;

/// <summary>
/// Gets the Task that executes the background operation.
/// </summary>
/// <remarks>
/// Will return <see langword="null"/> if the background operation hasn't started.
/// </remarks>
public virtual Task? ExecuteTask => _executeTask;

/// <summary>
/// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task that represents
/// the lifetime of the long running operation(s) being performed.
/// </summary>
/// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
/// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
/// <remarks>See <see href="https://learn.microsoft.com/dotnet/core/extensions/workers">Worker Services in .NET</see> for implementation guidelines.</remarks>
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous Start operation.</returns>
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

// Store the task we're executing
_executeTask = ExecuteAsync(_stoppingCts.Token);

// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executeTask.IsCompleted)
{
return _executeTask;
}

// Otherwise it's running
return Task.CompletedTask;
}

/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous Stop operation.</returns>
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executeTask == null)
{
return;
}

try
{
// Signal cancellation to the executing method
_stoppingCts!.Cancel();
}
finally
{
#if NET8_0_OR_GREATER
await _executeTask.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
#else
// Wait until the task completes or the stop token triggers
var tcs = new TaskCompletionSource<object>();
using CancellationTokenRegistration registration = cancellationToken.Register(s => ((TaskCompletionSource<object>)s!).SetCanceled(), tcs);
// Do not await the _executeTask because cancelling it will throw an OperationCanceledException which we are explicitly ignoring
await Task.WhenAny(_executeTask, tcs.Task).ConfigureAwait(false);
#endif
}

}

/// <inheritdoc />
public virtual void Dispose()
{
_stoppingCts?.Cancel();
}
}
}

BackgroundService クラスは、インターフェースである IHostedService と IDispose を実装しています。インターフェースそれぞれの役割は以下の通りです。

インターフェース名役割
IHostedServiceアプリが起動するときと終了するときに呼ばれる処理を定義するためのインターフェース
IDisposeガベージコレクションでは解放できないリソースを解放するための仕組みを提供するインターフェース

まずは IHostedService を少し深堀してみましょう。

interface IHostedService の解説

IHostedService には、2つ のメソッドしかありません。

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Defines methods for objects that are managed by the host.
/// 訳:ホストによって管理されるオブジェクトのためのメソッドを定義する
/// </summary>
public interface IHostedService
{
/// <summary>
/// Triggered when the application host is ready to start the service.
/// 訳:アプリケーションホストがサービスを開始する準備が整ったときに発動される
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted. 訳:開始処理が中止されたことを表す</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous Start operation. 約:非同期の開始処理を表す</returns>
Task StartAsync(CancellationToken cancellationToken);

/// <summary>
/// Triggered when the application host is performing a graceful shutdown. 訳:アプリケーションホストが正常にシャットダウン処理を実行しているときに呼ばれる
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful. 訳:シャットダウン処理がこれ以上正常に行われるべきでないことを表す</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous Stop operation. 訳:非同期の停止処理を表す</returns>
Task StopAsync(CancellationToken cancellationToken);
}
}

IHostedService にて実装を強要しているのは2つのメソッドです。

Task StartAsync(CancellationToken cancellationToken);

コメントの訳:アプリケーションホストがサービスを開始する準備が整ったときに発動される

  • アプリケーション起動時に呼ばれる
  • サービスの開始処理を書く場所(例: タイマー開始、バックグラウンドスレッドの起動、接続確立など)
  • cancellationToken が渡されるので、起動処理中にキャンセル(例: 起動失敗、強制終了)が可能

Task StopAsync(CancellationToken cancellationToken);

コメントの訳:アプリケーションホストが正常にシャットダウン処理を実行しているときに呼ばれる

  • アプリケーション終了時に呼ばれる
  • サービスの停止処理を書く場所(例: バックグラウンド処理をキャンセル、接続のクローズ、ログ出力)
  • cancellationToken が渡されるので、終了処理が可能(例: シャットダウンが一定時間で強制終了されるときに使う)

併せて考えると、 BackgroundService クラスは、IHostedService インターフェースを通じて上記2つの基礎的かつ具体的な実装を提供している、ということになります。

結果的にみると、BackgroundService クラスを継承することで、バックグラウンドで動作する標準的な機能を簡単に作成できるように設計、効率的に実装できます。

次です。

interface IDisposable の解説

//
// 概要:
// Provides a mechanism for releasing unmanaged resources. 訳:アンマネージリソースを解放する仕組みを提供する
public interface IDisposable
{
//
// 概要:
// Performs application-defined tasks associated with freeing, releasing, or resetting
// unmanaged resources. 訳:アンマネージリソースの解放・破棄・リセットに関連する、アプリケーション定義の処理を実行する
void Dispose();
}

C# には ガベージコレクション(GC) という仕組みがあり、使わなくなったオブジェクトが占有していた メモリ を自動で解放してくれます。 しかし、すべてのリソースが自動で解放されるわけではありません。たとえばファイルハンドルやデータベース接続、COMコンポーネントなどは、開発者自身が明示的に解放処理を記述する必要があります。

そこで登場するのが IDisposable インターフェース です。これを実装することで、リソース解放処理を Dispose メソッド にまとめ、標準的な方法で呼び出せるようになります。 また、using ステートメントを利用すれば、スコープを抜けたタイミングで自動的に Dispose が実行されるため、後片付け漏れを防ぐことができます。

BackgroundService クラスの確認は以上です。Worker.csにもどります。

Hello World をかくところ!

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

Console.WriteLine("Hello, World!");

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

ExecuteAsync メソッドの引数、stoppingToken.IsCancellationRequested != 真 の間、処理を続けます。

この while 句の中に定義したい実装を記述します。 " Hello World! " するならここです(笑)

今回は .NET Worker Service で常駐サービスを作るために SDKが作成するテンプレートを勉強しました。継承している抽象クラスやインターフェースのソースを確認し、目的を調べることができました。

これで Worker Service で書けそうです。実際に書いてみて沢山の失敗を経験することにします(笑)

未経験から始める
システムエンジニア

一生モノのITスキルを身につけよう

あなたの経験とスキルを
ディアシステムで発揮してください!