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

今回の記事、果たしてこんな事して意味があるのか不明です。

ただ、コンシューマ向けのWindowsに搭載のIISはExpress版なので多少制限があってそれを、ASP.NET CoreのKestrelを使って回避しようとした時に、Window上でサービス化してみたのでその手順をまとめてみたいと思います。内容はほ、ほぼMSDNのここに書いてあることと同一になります。

環境は、VisualStudo 2017 + ASP.NET Core 2.0 + .NET Framework 4.7.1で試しています。

プロジェクトの準備

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

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

f:id:Takachan:20171220220817p:plain

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

f:id:Takachan:20171220221412p:plain

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

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

f:id:Takachan:20171220223503p:plain

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

f:id:Takachan:20171220224435p:plain

コードの編集

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

// 自動生成されたコード
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が呼び出されるはずです。

f:id:Takachan:20171220225429p:plain

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

f:id:Takachan:20171220225456p:plain

サービスの登録

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

f:id:Takachan:20171220225753p:plain

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

f:id:Takachan:20171220225829p:plain

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

f:id:Takachan:20171220225941p:plain

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

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

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

f:id:Takachan:20171220230148p:plain

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

f:id:Takachan:20171220230259p:plain

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

f:id:Takachan:20171220230516p:plain

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

補足

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

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

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

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

f:id:Takachan:20171220231202p:plain