Hangfireに.NET Core 3.1で最速入門する

## はじめに

Webアプリをつくるときにどうしても問題になるのがレスポンスだと思います。
時間がかかる処理をWeb画面でポチッとした時に実行されるシステムを作る時、あなたならどうしますか？

30秒とかならまだユーザに待ってもらえるかも知れませんが、
5時間かかる処理だったらどうしましょう？？

そこで有効な考え方がメッセージキューを使用した仕組みです。
APサーバーからはキューに「これやっとくように言っておいて」と伝えると、
キューの内容を購読しているワーカーが順次やっていくような仕組みです。

そんな時に使いやすいHangfireというフレームワーク？ライブラリ？を紹介します。

### 早く結論をくれ

今回の記事に載せた手順を経ると下記リポジトリのようなものができます。

https://github.com/yoh1496/hangfire-dotnet3.1/tree/initial

### 環境

この記事の内容を試すのに使った環境は

- OS: Windows 10
- RAM: 8GB

です。

## Hangfireとは？

ジョブのキューイング／実行ができるフレームワークです。

`rabbitMQ`や`ActiveMQ`といったメッセージキュー系のソフトウェアを使用すれば
同じことは実現できますが、`Hangfire`には以下の利点があります。

- .NET（NuGet）の巨大な恩恵を授かることができる
- ダッシュボードまで準備されているいたれりつくせりぶり

しかし、だいぶマイナーなので調べても日本語で書いてある解説が少ないのが難点でしょうか。

### .NET Core（OSS版.NET Framework）公開で敷居が下がった

Microsoftがオープンソース版.NET Frameworkであるところの`.NET Core`を公開しました。
Hangfireは.NET Coreにも対応しており、これによってだいぶ敷居が下がっています。が、依然マイナーですね。

### まだ更新されているのか？

私は4年ぐらい前からHangfireを使用しているんですが、まだ開発は続いています。

https://github.com/HangfireIO/Hangfire


## 早速入門する

### dotnet sdk のインストール

下記リンクからdotnet sdk 3.1をダウンロード／インストールしてください。
https://dotnet.microsoft.com/download

インストールが完了すると以下のようにdotnetコマンドを使用可能になります。

```bash
$ dotnet --version
3.1.100
```

### ソリューションディレクトリの作成

今回は`client`プロジェクトと`server`プロジェクトを作ろうと思うので、
それぞれを束ねるソリューションを作成します。

```bash
$ dotnet new sln -o hangfire-dotnet3
The template "Solution File" was created successfully.

$ cd hangfire-dotnet3 # ソリューションのディレクトリに移動

$ ls
hangfire-dotnet3.sln
```

### 各プロジェクトの追加

先程作成したソリューションに`client`プロジェクトを追加します

```bash
$ dotnet new webapp -o client
The template "ASP.NET Core Web App" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/3.1-third-party-notices for details.
f
Processing post-creation actions...
Running 'dotnet restore' on client\client.csproj...
  C:\dev\hangfire-dotnet3\client\client.csproj の復元が 165.82 ms で完了しました。

Restore succeeded.

$ dotnet sln add client # プロジェクトの追加
プロジェクト `client\client.csproj` をソリューションに追加しました。
```

同様に`server`プロジェクトも追加します

```bash
$ dotnet new console -o server

$ dotnet sln add server
```

上記工程で client, server フォルダにそれぞれテンプレートが展開されたと思います。

※注意
HangfireではジョブをキューするUIを提供するWebサーバ／APサーバを `client`と呼びます。
一方で、キューされたジョブを待ち受け、順次実行するワーカを `server` と呼びます。
よく混同するので注意！

### JobStorageの準備

キューとして使えるものは以下のページの `Storage` セクションにあります。

https://www.hangfire.io/extensions.html

今回はmongoDB使っていきたいと思います。

```bash
docker run -d -p 27017:27017 mongo:4.0-xenial
```

終わり。
（docker使えない場合は https://www.mongodb.com/download-center/community?jmp=docs からインストールすると良いと思います。）


### Serverの実装

必須パッケージをインストールする

```bash
$ cd server
$ dotnet add package Hangfire.Core -v 1.7.8
$ dotnet add package Hangfire.Mongo -v 0.6.5
```

すると `server/server.csproj` ファイルに依存パッケージが追記されます。

`server/Program.cs` には以下のようにコードを書いて、先程Dockerで立てたmongoを参照するように設定します。

```cs
using System;
using Hangfire;
using Hangfire.Mongo;

namespace server {
    class Program {
        static void Main(string[] args) {
            GlobalConfiguration.Configuration
                .UseMongoStorage("mongodb://localhost", "ApplicationDatabase");
            using(var server = new BackgroundJobServer()) {
                Console.WriteLine("Started BackgroundJobServer. Press Enter to exit.");
                Console.ReadLine();
            }
        }
    }
}
```

サーバーはこれだけで完成です。

```bash
$ dotnet run .
```

コマンドで以下のような表示が出ればオッケーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/6321c04e-8000-b413-b9cc-491632779b55.png)


### Clientの実装

Clientにも同様に必須パッケージをインストールします。

```bash
$ cd client 
$ dotnet add package Hangfire -v 1.7.8
$ dotnet add package Hangfire.AspNetCore -v 1.7.8
$ dotnet add package Hangfire.Mongo -v 0.6.5
```

次に、`client/StartUp.cs` を編集します。

```cs
        public void ConfigureServices(IServiceCollection services) {
            services.AddRazorPages();

            services.AddHangfire(config => {
                string connectionString = "mongodb://localhost";   # 本来は環境変数で指定する
                string databaseName = "ApplicationDatabase";   # 本来は環境変数で指定する
                var storageOptions = new MongoStorageOptions {
                    MigrationOptions = new MongoMigrationOptions {
                    Strategy = MongoMigrationStrategy.Drop,
                    },
                };
                config.UseMongoStorage(connectionString, databaseName, storageOptions);
            });
        }
```

本来は接続文字列とかは環境変数で指定できるように変更しておくべきかと思ったんですが、
とりあえずサンプルということでざざっと直書き。

ここまで書いて以下のコマンドで`Client`を起動します。

```
$ cd client
$ dotnet run .
```

そしてブラウザで http://localhost:5000/hangfire にアクセスすると以下のようなダッシュボードが開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/255a08bc-93ef-2d06-b031-a2c6213eefdc.png)

### Producerの実装

上記までの手順で、タスク管理して実行する機構ができました。
次はタスクを実際にキューするアプリを作成します。

```
$ dotnet new console -o app

$ cd app
$ dotnet add package Hangfire.Core -v 1.7.8
$ dotnet add package Hangfire.Mongo -v 0.6.5
```

app/Program.cs ファイルを編集します

```cs
using System;
using System.Threading;
using Hangfire;
using Hangfire.Mongo;

namespace app {
    class Program {
        static void Main(string[] args) {
            GlobalConfiguration.Configuration
                .UseMongoStorage("mongodb://localhost", "ApplicationDatabase");

            int Cnt = 10;
            for (int i = 0; i < Cnt; i++) {
                BackgroundJob.Enqueue(() => Console.WriteLine($"task #{i}"));
                Console.WriteLine($"Enqueued task#{i}");
            }
        }
    }
}
```

単純にfor文で10個タスクをキューに入れていきます。

```bash
$ dotnet run .
Enqueued task#0
Enqueued task#1
Enqueued task#2
Enqueued task#3
Enqueued task#4
Enqueued task#5
Enqueued task#6
Enqueued task#7
Enqueued task#8
Enqueued task#9
```

## 実行

`client` と `server` を起動した状態で `app` を実行してみましょう。
`server` を複数実行させてみると分散されて実行されている様子が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/808a28fc-d544-1518-ec40-b056e600dd90.png)

ダッシュボードはこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/f42e0445-94c2-8afa-86c7-32bf64eeb875.png)

ジョブ履歴もこのように参照することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/5c52c11f-d337-893c-b359-39ed2bdf52f9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/1577cbb3-7a25-e827-df00-9825f3c1def9.png)

## 終わりに

`Hangfire`というジョブをキューしてワーカーに実行させるフレームワークについて紹介しました。

これだけで分散実行してダッシュボードで管理できるのはすごく便利です。
今回はConsole.WriteLineするだけという単純なジョブ実行でしたが、
共通ライブラリでクラスを定義することでもっと複雑な処理をさせることが可能です。

また、今回は「ダッシュボード」「ワーカー」「タスクのキューイング実行」と3構成で実装しましたが、
ダッシュボードアプリにコントローラを実装して、APIを通してキューイングさせるような実装も可能です。