BackgroundService を使用して Windows サービスを作成する

.NET Framework 開発者は、おそらく Windows サービス アプリに精通しています。 .NET Core と .NET 5 より前は、.NET Framework に依存する開発者は、バックグラウンド タスクを実行したり、実行時間の長いプロセスを実行したりするために、Windows サービスを作成できました。 この機能は引き続き使用できます。また、Windows サービスとして実行される Worker サービスを作成できます。

このチュートリアルで学習する内容は次のとおりです。

  • .NET ワーカー アプリを 1 つのファイル実行可能ファイルとして発行します。
  • Windows サービスを作成します。
  • Windows サービスとして BackgroundService アプリを作成します。
  • Windows サービスを開始および停止します。
  • イベント ログを表示します。
  • Windows サービスを削除します。

ヒント

".NET でのワーカー" のサンプル ソース コードはすべて、サンプル ブラウザーでダウンロードできます。 詳細については、コード サンプルの参照: .NET でのワーカーに関するページをご覧ください。

重要

.NET SDK をインストールすると、Microsoft.NET.Sdk.Worker と worker テンプレートもインストールされます。 つまり、.NET SDK をインストールすると、dotnet new worker コマンドを使用して新しい worker を作成できるようになります。 Visual Studio を使用している場合は、テンプレートは、オプションの ASP.NET と Web 開発ワークロードがインストールされるまで非表示になります。

前提条件

新しいプロジェクトを作る

Visual Studio を使用して新しい Worker サービス プロジェクトを作成するには、[ファイル]>[新規]>[プロジェクト] を選択します。[新しいプロジェクトの作成] ダイアログで "Worker サービス" を検索し、Worker サービス テンプレートを選択します。 .NET CLI を使用する場合は、作業ディレクトリで好みのターミナルを開きます。 dotnet new コマンドを実行し、<Project.Name> を目的のプロジェクト名に置き換えます。

dotnet new worker --name <Project.Name>

.NET CLI の new worker サービス プロジェクト コマンドの詳細については、「dotnet new worker」を参照してください。

ヒント

Visual Studio Code を使用している場合は、統合ターミナルから .NET CLI コマンドを実行できます。 詳細については、Visual Studio Code の統合ターミナルに関する記事を参照してください。

NuGet パッケージのインストール

.NET の IHostedService の実装からネイティブ Windows サービスと相互運用するには、Microsoft.Extensions.Hosting.WindowsServices NuGet パッケージをインストールする必要があります。

これを Visual Studio からインストールするには、[NuGet パッケージの管理] ダイアログを使用します。 "Microsoft.Extensions.Hosting.WindowsServices" を検索してインストールします。 .NET CLI を使用する場合は、dotnet add package コマンドを実行します。

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

.NET CLI の add package コマンドの詳細については、「dotnet add package」を参照してください。

パッケージが正常に追加されると、プロジェクト ファイルに次のパッケージ参照が含まれるはずです。

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>

プロジェクト ファイルの更新

このワーカー プロジェクトでは、C# の null 許容参照型を利用します。 プロジェクト全体でそれらを有効にするには、次のように、プロジェクト ファイルを適切に更新します。

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

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

このようにプロジェクト ファイルを変更すると、<Nullable>enable<Nullable> ノードが追加されます。 詳細については、「Null 許容コンテキストの設定」を参照してください。

サービスの作成

新しいクラスを JokeService.cs という名前のプロジェクトに追加し、その内容を次の C# コードに置き換えます。

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

前のジョーク サービスのソース コードでは、GetJoke メソッドという機能の一部が公開されています。 これは、メソッドを返す string であり、ランダムなプログラミング ジョークを表します。 クラス スコープ _jokes フィールドは、ジョークの一覧を格納するために使用されます。 ランダムなジョークが一覧から選択されて、返されます。

Worker クラスの再書き込み

テンプレートの既存の Worker を次の C# コードに置き換え、ファイルの名前を WindowsBackgroundService.cs に変更します。

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

上のコードでは、JokeServiceILogger と共に挿入されます。 両方とも、クラスで private readonly フィールドとして使用できます。 ExecuteAsync メソッドでは、ジョーク サービスがジョークを要求して、それをロガーに書き込みます。 この場合、ロガーは Windows イベント ログ Microsoft.Extensions.Logging.EventLog.EventLogLogger によって実装されます。 ログはイベント ビューアーに出力され、確認できるようになります。

注意

既定では、"イベント ログ" の重大度は Warning です。 これは構成できますが、デモンストレーションのため、WindowsBackgroundServiceLogWarning 拡張メソッドでログを作成します。 EventLog レベルを明示的に対象とするには、appsettings.{Environment}.json にエントリを追加するか、EventLogSettings.Filter 値を指定します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

ログ レベルの構成の詳細については、.NET でのログ プロバイダーの Windows EventLog の構成に関する記事を参照してください。

Program クラスの再書き込み

テンプレートの Program.cs ファイルの内容を、次の C# コードに置き換えます。

using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = ".NET Joke Service";
});

LoggerProviderOptions.RegisterProviderOptions<
    EventLogSettings, EventLogLoggerProvider>(builder.Services);

builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();

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

AddWindowsService 拡張メソッドにより、アプリが Windows サービスとして動作するように構成されます。 サービス名は ".NET Joke Service" に設定されます。 ホステッド サービスが、依存関係の挿入に登録されます。

サービスの登録の詳細については、「.NET での依存関係の挿入」を参照してください。

アプリの発行

.NET Worker サービス アプリを Windows サービスとして作成するには、アプリを単一ファイルの実行可能ファイルとして発行することをお勧めします。 自己完結型の実行可能ファイルを使用すると、ファイル システムのあちこちに依存ファイルが散らばることがないため、エラーは発生しにくくなります。 ただし、Windows サービス コントロール マネージャーのターゲットにすることができる *.exe ファイルを作成する限り、別の発行モダリティを選択することは可能であり、完全に許容されます。

重要

(*.exe の代わりに) *.dll をビルドする発行方法もあります。発行したアプリを Windows サービス コントロール マネージャーを使用してインストールするときは、.NET CLI に委任して DLL を渡します。 詳細については、「.NET CLI: dotnet コマンド」を参照してください。

sc.exe create ".NET Joke Service" binpath="C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
    <OutputType>exe</OutputType>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

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

上のプロジェクト ファイルの強調表示された行では、次の動作が定義されています。

  • <OutputType>exe</OutputType>: コンソール アプリケーションを作成します。
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: 単一ファイルの発行を有効にします。
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: win-x64RID を指定します。
  • <PlatformTarget>x64</PlatformTarget>: 64 ビットのターゲット プラットフォーム CPU を指定します。

Visual Studio からアプリを発行するには、永続化された発行プロファイルを作成します。 発行プロファイルは XML ベースであり、ファイル拡張子は .pubxml です。 Visual Studio は、このプロファイルを使用してアプリを暗黙的に発行します。一方、.NET CLI を使用している場合は、それが使用する発行プロファイルを明示的に指定する必要があります。

ソリューション エクスプローラーでプロジェクトを右クリックし、[発行] を選択します。次に、[発行プロファイルの追加] を選択してプロファイルを作成します。 [発行] ダイアログで、[ターゲット] として [フォルダー] を選択します。

The Visual Studio Publish dialog

既定の [場所] のままにして、[完了] を選択します。 プロファイルが作成されたら、[すべての設定を表示] を選択して、[プロファイルの設定] を確認します。

The Visual Studio Profile settings

次の設定が指定されていることを確認します。

  • [配置モード]: 自己完結
  • [単一ファイルの作成]: オン
  • [ReadyToRun コンパイルを有効にする]: オン
  • [未使用のアセンブリをトリミングする (プレビュー)]: オフ

最後に、[発行] を選択します。 アプリがコンパイルされ、結果の .exe ファイルが /publish 出力ディレクトリに発行されます。

または、.NET CLI を使用してアプリを発行することもできます。

dotnet publish --output "C:\custom\publish\directory"

詳細については、dotnet publish をご覧ください。

重要

.NET 6 で <PublishSingleFile>true</PublishSingleFile> 設定を使用してアプリのデバッグを試みる場合は、アプリをデバッグすることができません。 詳細については、「'PublishSingleFile' .NET 6 アプリおデバッグ時に CoreCLR にアタッチできない」を参照してください。

Windows サービスを作成する

PowerShell を使い慣れておらず、サービス用のインストーラーを作成したい場合は、「Windows サービス インストーラーを作成する」を参照してください。 それ以外の場合で、Windows サービスを作成するには、ネイティブ Windows サービス コントロール マネージャー (sc.exe) の create コマンドを使用します。 PowerShell を管理者として実行します。

sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe"

ヒント

ホスト構成のコンテンツ ルートを変更する必要がある場合は、binpath を指定するときにコマンドライン引数として渡すことができます。

sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path"

次のような出力メッセージが表示されます。

[SC] CreateService SUCCESS

詳細については、「sc.exe create」を参照してください。

Windows サービスを構成する

サービスを作成したら、必要に応じてそれを構成できます。 サービスの既定値に問題がなければ、「サービスの機能を確認する」セクションに進みます。

Windows サービスによって、回復構成オプションが提供されます。 現在の構成に対してクエリを実行するには、sc.exe qfailure "<Service Name>" (ここで、<Service Name> はご利用のサービスの名前です) コマンドを使用して、現在の回復構成の値を読み取ります。

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :

このコマンドを使用すると、回復構成が出力されます。これは、まだ構成されていないため、既定値です。

The Windows Service recovery configuration properties dialog.

回復を構成するには、sc.exe failure "<Service Name>" を使用します。ここで、<Service Name> はご利用のサービスの名前です。

sc.exe failure ".NET Joke Service" reset=0 actions=restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS

ヒント

回復オプションを構成するには、ターミナル セッションを管理者として実行する必要があります。

正常に構成されたら、sc.exe qfailure "<Service Name>" コマンドを使用して、もう一度その値に対してクエリを実行できます。

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :
        FAILURE_ACTIONS              : RESTART -- Delay = 60000 milliseconds.
                                       RESTART -- Delay = 60000 milliseconds.
                                       RUN PROCESS -- Delay = 1000 milliseconds.

構成された再起動の値が表示されます。

The Windows Service recovery configuration properties dialog with restart enabled.

サービス回復オプションと .NET BackgroundService インスタンス

.NET 6 では、新しいホスティングの例外処理動作が .NET に追加されています。 BackgroundServiceExceptionBehavior 列挙型が Microsoft.Extensions.Hosting 名前空間に追加され、例外がスローされたときのサービスの動作を指定するために使用されます。 次の表は、使用可能な値の一覧です。

オプション 説明
Ignore BackgroundService でスローされた例外を無視します。
StopHost ハンドルされない例外がスローされると、IHost は停止します。

.NET 6 より前の既定の動作は Ignore であり、"ゾンビ プロセス" (何も行わない実行中のプロセス) が発生していました。 .NET 6 では、既定の動作は StopHost であり、例外がスローされたときにホストは停止します。 ただし、クリーンに停止します。つまり、Windows サービス管理システムによってサービスが再起動されることはありません。 サービスの再起動を正常に許可するには、0 以外の終了コードを使って Environment.Exit を呼び出すことができます。 次の強調表示された catch ブロックを考えてみましょう。

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

サービスの機能を確認する

Windows サービスとして作成されたアプリを表示するには、[サービス] を開きます。 Windows キー (または Ctrl + Esc キー) を押し、"サービス" を検索します。 [サービス] アプリから、その名前でサービスが見つかるはずです。

重要

既定では、通常の (管理者以外の) ユーザーは Windows サービスを管理できません。 このアプリが想定どおりに機能することを検証するには、管理者アカウントを使用する必要があります。

The Services user interface.

サービスが期待したとおりに機能していることを確認するには、次のようにする必要があります。

  • サービスの開始
  • ログを表示する
  • サービスの停止

重要

アプリケーションをデバッグするには、Windows サービスのプロセス内でアクティブに実行されている実行可能ファイルをデバッグしようとして "いない" ことを確実にします

Unable to start program.

Windows サービスを開始する

Windows サービスを開始するには、sc.exe start コマンドを使用します。

sc.exe start ".NET Joke Service"

次のような出力が表示されます。

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 2  START_PENDING
                            (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x7d0
    PID                : 37636
    FLAGS

サービスの [状態] が、[START_PENDING] から [実行中] に遷移します。

ログを表示する

ログを表示するには、イベント ビューアーを開きます。 Windows キー (または Ctrl + Esc キー) を押し、"Event Viewer" を検索します。 [イベント ビューアー (ローカル)]>[Windows ログ]>[アプリケーション] ノードを選択します。 アプリの名前空間と一致する [ソース][警告] レベル エントリが表示されます。 そのエントリをダブルクリックするか、右クリックして [イベントのプロパティ] を選択し、詳細を表示します。

The Event Properties dialog, with details logged from the service

[イベント ログ] でログを確認したら、サービスを停止する必要があります。 1 分間に 1 回ランダムなジョークをログに記録するように設計されています。 これは意図的な動作ですが、運用サービスとしては実用的ではありません

Windows サービスを停止する

Windows サービスを停止するには、sc.exe stop コマンドを使用します。

sc.exe stop ".NET Joke Service"

次のような出力が表示されます。

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 3  STOP_PENDING
                            (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x0

サービスの [状態] が、[STOP_PENDING] から [停止] に遷移します。

Windows サービスを削除する

Windows サービスを削除するには、ネイティブ Windows サービス コントロール マネージャー (sc.exe) の delete コマンドを使用します。 PowerShell を管理者として実行します。

重要

サービスが [停止] 状態でない場合は、すぐに削除されません。 delete コマンドを発行する前に、サービスが停止していることを確認してください。

sc.exe delete ".NET Joke Service"

次のような出力メッセージが表示されます。

[SC] DeleteService SUCCESS

詳細については、「sc.exe delete」を参照してください。

関連項目

Next