ASP.net Core でHangfireを用いたジョブスケジューラを作成する

# はじめに

`Hangfire.io` タグの記事も、早いものでもう7記事目になりました。
自分しか書いていないので、記事数をカウントするにはもってこいのタグですね（何）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/349ae31e-650b-2ac6-a8cf-85087a69ede9.png)


今まではコンソールアプリからタスクを登録する方式をとっていたのですが、
今回はそれをASP.net Coreで構築したWebページから実行します。

やっていることは変わらないんですが、
タスク受付がWebページになることで、Hangfireの威力を実感できると思います。

# 実装

今回も今回とて、[前回記事](https://qiita.com/yoh1496/items/bbf8764fa743bd36f13d)の内容から変更していきます。

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

## Razor pageを作成する

何を思ったか、 `client` を Razor page 使うプロジェクトで作成していたので、
今回はRazor pageでやっていきます。

普段はASP.Net Core MVCを使用しているので、
Razor pages ってあまり触ったことなく知らなかったんですが、
下記コマンドでページのテンプレートを作成可能です。

```powershell
dotnet new page -o Pages -n Create # generating Pages/Create.cshtml
```

```powershell
dotnet new page -h

# 中略
Razor Page (C#)
Author: Microsoft
Description: A Razor page with or without a page model
Options:
  -na|--namespace     namespace for the generated code
                      string - Optional
                      Default: MyApp.Namespace

  -np|--no-pagemodel  create page without a PageModel 
                      bool - Optional
                      Default: false / (*) true
```

とあるので、 `-na` オプションでnamespaceも指定できるみたいですね。

とはいえ、今回はRazorの使い方には何の価値もないと思うので、
ザザザッと作っていきます。

### List ページを作成する

作成したタスク一覧を表示できるようにします。

```html
// List.cshtml
<table class="table">
    <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">Name</th>
            <th scope="col">Status</th>
            <th scope="col">Result</th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.tasks)
{
    <tr>
        <th scope="row">@item.Id</th>
        <td>@item.Name</td>
        <td>@item.Status</td>
        <td>@item.Result</td>
    </tr>
}
    </tbody>
</table>
```

（Razorのコードってハイライトしてくれないのね）


```csharp
// List.cshtml.cs
public class ListModel : PageModel {
    private readonly ITaskService _taskService;
    private readonly ILogger<ListModel> _logger;

    public List<TaskModel> tasks { get; set; }

    public ListModel(ITaskService taskService, ILogger<ListModel> logger) {
        this._taskService = taskService;
        this._logger = logger;
    }
    public async Task<IActionResult> OnGet() {
        this.tasks = await _taskService.Find(x => true);
        return Page();
    }
}
```

このように、Razor の PageModelでもDependency Injectionできるように
コンストラクタに `ITaskService taskService` として実装しておきます。

（こうしておくことで、TaskServiceクラスの実装を待たずにWeb画面を実装できるのです。便利。）

TaskServiceはMongoDBにタスクを登録したり、登録されたタスクを取得したりできるサービスにします。

できたのはこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/7aea7974-a905-7b81-7349-0bb8c116da60.png)

### Create ページを作成する

同様にタスクを登録するページを作成していきます。

```html
// Create.cshtml
<p>Enter a task name:</p>

<form method="post">
    Name:
    <input asp-for="taskName" />
    <input type="submit" />
</form>
```

POSTで受け取って保存していきます。

```csharp
// Create.cshtml.cs
public async Task<IActionResult> OnPostAsync() {
    if (!ModelState.IsValid) {
        return Page();
    }

    var result = await _taskService.RegisterTask(this.taskName);
    var jobId = BackgroundJob.Enqueue<ReverseStringTask>(x => x.Execute(result.Id.ToString(), null));

    // for debugging
    this._logger.LogDebug(JsonSerializer.Serialize(result));
    this._logger.LogDebug(JsonSerializer.Serialize(new { jobId = jobId }));
    return RedirectToPage("List");
}
```

重要なのは、

```csharp
BackgroundJob.Enqueue<ReverseStringTask>(x => x.Execute(result.Id.ToString(), null));
```

として `RegisterTask` でMongoDBにタスクデータを保存したあとに、
その Id を引数に `ReverseStringTask` というタスクの Execute関数の実行を
キューに入れているところです。
（今回はReverseStringTaskというクラスでタスクを実装します。）

当然、ReverseStringTaskもDependency Injectionしたいところですが、
今回は面倒くさかったので省いてしまいました。

すごくしょぼいけど、登録ページ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/1d2f2587-4c98-8ae4-8d8a-5d22d6e14189.png)


## タスクを実装する

先程ちょろっと名前だけ出しましたが、
今回は `ReverseStringTask` というクラスでタスクを実装します。

```csharp
// ReverseStringTask.cs
public class ReverseStringTask {
    private ITaskService _taskService;

    public ReverseStringTask(ITaskService _taskService) {
        this._taskService = _taskService;
    }
    public void Execute(string objectId, PerformContext context) {
        Console.WriteLine($"Super heavy task #{context.BackgroundJob.Id}");
        var taskModel = _taskService.Find(x => x.Id == MongoDB.Bson.ObjectId.Parse(objectId)).Result[0];
        taskModel.Status = "DONE";
        taskModel.Result = new string(taskModel.Name.Reverse().ToArray());
        _taskService.Update(objectId, taskModel);
        Console.WriteLine($"Done: {taskModel.Name}");
    }
}
```

読んで字のごとく、
タスクの `Name` を Reverse して `Result` に格納して保存します。

今回もTaskServiceを DependencyInjection します。
（しまった Logger クラスも DI すればよかった。）

Hangfire の `PerformContext` を引数にとっていること以外、
何の変哲もないクラスであることがわかります。
PerformContextを使用しないケースでは、
本当にHangfireに非依存なクラスで大丈夫ということです。

## 起動コードを修正する

あとは `TaskServer` インスタンスを DependencyInjection できるように、
起動コードを修正します。

```csharp
// server/Program.cs
services.AddTransient<ITaskService, TaskService>();
```

# 実行

では実行していきます

## タスクの登録

「テスト」という名前でタスクを登録してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/6d99b0ac-f9bb-5c7c-862d-e242b6d2f44c.png)

タスクリストに登録されたことがわかります。
ここで、サーバーを動かしてみます。
（サーバーとはタスクを実行する側。 `Hangfire Server` ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/348b0180-7b0b-0083-297b-d3ad8d7ebe71.png)

先程のタスクのResultに「テスト」の逆転、「トステ」が格納されました。

同じようにして、「テスト２」を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/984f1b06-709e-8e52-51b8-c2e384a39cf5.png)

今度はServerが起動したままなので、
直後はCREATEDになりますが、F5でリロードすると即座にDONEに切り替わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/22ea726b-ef6f-2ce6-32ed-e3e9342e26ed.png)


# 終わりに

今回は今まで作ってきたジョブスケジューラーにWeb画面を取り付けて、
Webサーバー（Hangfire Client）から登録したタスクを
別プロセス（Hangfire Server）で実行するデモをしてみました。

使用コードはこちら↓
https://github.com/yoh1496/hangfire-dotnet3.1/tree/web_client

今回は入力された文字を逆転させるといった、
しょうもないタスクを実行させましたが、
実行させたタスククラスは何の変哲もないただのクラスですので、
使い方次第であんなことやこんなことも実行させることが可能です。

メッセージキューやら何やら色々あるご時世になぜHangfireを使うのかって？
それはこれから考える！