ASP.NET Core 2.0をWindowsサービス化する

タイトルの通り、APS .NET Core2.0 の Kestrel を Windows サービス化してみようと思います。

Windows Server に搭載されている IIS と違って Windows Proとかの Express版(機能制限版IIS)は TCP 接続数などに制限があるのでそれを、ASP.NET Core 付属の Kestrel を使って少しでも制限を回避したいと思います。

そこで、Kestrel を Window 上でサービス化してみたのでその手順を紹介したいと思います。

ちなみに、以前はこの MSDN を参照すれば問題なかったのですが、.NET Core 3 → .NET 5, 6 と後続のバージョンが出てきたことによりプログラミングモデルが変化しているので改めてここで紹介したいと思います。

確認環境

作成・動作確認環境は以下の通りです。

  • ASP.NET Core 2.0
  • .NET Framework 4.7.1
  • Visual Studio 2017

プロジェクトの準備

まずはプロジェクトの準備です。

VisualStudio上で新規作成から、Visual C# > Web > ASP.NET Core Web アプリケーション を選択します。

次に表示される、「新しい ASP.NET Core アプリケーション」で以下の赤枠の箇所、左側を".NET Framework"、右側を"ASP .NET Core2.0"に指定します。

そうするとソリューションとプロジェクトが作成されます。

次に、NuGetから、"Microsoft.AspNetCore.Hosting.WindowsServices"を選択します。

検索ボックスにパッケージ名を入力し検索後、インストールします。

コードの編集

次にコードの編集です。プロジェクトのメインメソッドのテンプレートが以下の通り配置されています。

// 自動生成されたコード
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();
}

これを以下のように変更します。

public static void Main(string[] args)
{
    // サービスとして起動するかどうかのフラグ
    // デバッグ実行時にサービスとして実行しないようにする
    bool isService = !(Debugger.IsAttached || args.Contains("--console"));

    // サービス実行時のルートディレクトリの指定
    string pathToContentRoot = Directory.GetCurrentDirectory();
    if (isService)
    {
        string pathToExe = Process.GetCurrentProcess().MainModule.FileName;
        pathToContentRoot = Path.GetDirectoryName(pathToExe);
    }

    IWebHost host = 
        WebHost.CreateDefaultBuilder(args)
            .UseKestrel()
            .UseUrls("http://*:80/") // とりあえず80番で起動
            .UseContentRoot(pathToContentRoot)
            .UseStartup<Startup>()
            .Build();

    // サービスかどうかで起動方法を分ける
    if (isService)
    {
        host.RunAsCustomService();
    }
    else
    {
        host.Run();
    }
}

また、上述のコードで使用しているRunAsCustomServiceメソッドの実装を以下の通り追加します。

using System.ServiceProcess;
using Microsoft.AspNetCore.Hosting.WindowsServices;

/// <summary>
/// IWebHostの拡張メソッド
/// </summary>
public static class WebHostServiceExtensions
{
    public static void RunAsCustomService(this IWebHost host)
    {
        var webHostService = new CustomWebHostService(host);
        ServiceBase.Run(webHostService);
    }
}

/// <summary>
/// このプロセスがサービスである事を表します。
/// </summary>
public class CustomWebHostService : WebHostService
{
    public CustomWebHostService(IWebHost host) : base(host) { }

    protected override void OnStarting(string[] args)
    {
        Console.WriteLine("OnStarting method called.");
        base.OnStarting(args);
    }

    protected override void OnStarted()
    {
        Console.WriteLine("OnStarted method called.");
        base.OnStarted();
    }

    protected override void OnStopping()
    {
        Console.WriteLine("OnStopping method called.");
        base.OnStopping();
    }
}

サービスの実装

今回はWeb APIをソリューション作成時に選択していたので、最初からValuesControllerがサンプルとして入っています。上記のコードを変更した状態でデバッグ実行するとメインメソッドで以下のようにhost.Runが呼び出されるはずです。

次に自動的に既定のブラウザが立ち上がってHTTP Getが呼ばれ以下応答があると思います。

サービスの登録

まずはプロジェクトを発行します。

今回はローカルディスク上のE:\www rootへ発行します。

なんとフォルダに114個もアセンブリが配置されますが気にしないように…

で、次にコマンドプロンプトを管理者権限で立ち上げて以下コマンドを入力します。

sc create WebTest binPath="E:\www root\WebApplication1.exe"
sc start WebTest

実行結果は以下のようになっていれば成功です。

OSのサービス管理画面上でも実行されている表示になっています。

この状態で、ブラウザのアドレスバーに"http://localhost/api/values"と入力すると、先ほどと同じように以下のデータが取得できます。

これで、サービスで動作していることが確認できました。

補足

x86のみサポート?のようで、ビルドをx64へ変更すると発行時にエラーとなります。

上記のサービスを削除したいときはコマンドプロンプトにて以下コマンドを打ってください。

sc stop WebTest
sc delete WebTest binPath="E:\www root\WebApplication1.exe"

以下の表示になっていれば削除できているはずです。