Hangfire のJob実行時にPerformContextを使用する


# はじめに

みなさん、Hangfire Serverを使用していて、
Job実体の関数を実行する際に jobId を始めとする、
コンテキストにアクセスしたいと思ったことはありませんか？

（そもそも `Hangfire` 記事があまり読まれていない！ 2020/04/24現在）

今回はそのコンテキストの使用方法を調査しましたので、記事にしたためます。

# コンテキスト？

「会話のコンテキスト」って言うように、背景とか文脈みたいな意味ですね。

Hangfireだと `PerformContext` だったり、 `CreatedContext` とか
タスクの状態によって様々なコンテキストが定義されています。

[PerformContext Class](https://api.hangfire.io/html/T_Hangfire_Server_PerformContext.htm)

今回はジョブがなんという `JobId` で実行されているのか
知りたいケース（例えばJobIdでフォルダを作るなど）で、
それを知るための方法を調査していたらコンテキストに行き着きました。

## フォーラムにおける JobIdの取得方法

[Use Hangfire job in the code](https://discuss.hangfire.io/t/use-hangfire-job-id-in-the-code/2621/3)

> For achieving your goal, just add PerformContext to your job method:
>
> ```csharp
> public void SendEmail(string name, PerformContext context)
> {
>     string jobId = context.BackgroundJob.Id;
> }
> ```
>
> And pass null for it when enqueuing a job:
>
> ```csharp
> BackgroundJob.Enqueue(() => SendEmail(name, null));
> ```

とありますね。
実行関数にPerformContext型の引数を追加して、キューするときにnullを渡せばいいいと。

#### コードの参照

実際にソースコードを見てみます。

https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Server/CoreBackgroundJobPerformer.cs#L226

```csharp
var parameters = context.BackgroundJob.Job.Method.GetParameters();
var result = new List<object>(context.BackgroundJob.Job.Args.Count);

for (var i = 0; i < parameters.Length; i++)
{
    var parameter = parameters[i];
    var argument = context.BackgroundJob.Job.Args[i];

    var value = Substitutions.ContainsKey(parameter.ParameterType) 
        ? Substitutions[parameter.ParameterType](context) 
        : argument;

    result.Add(value);
}
```

確かに、`parameters` の型をチェックして、
一致するもののargument（キュー時に渡された引数）とSubstitutionsの関数で置き換えています。
問題のSubstitutionsは何かというと

https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Server/CoreBackgroundJobPerformer.cs#L31

```csharp
internal static readonly Dictionary<Type, Func<PerformContext, object>> Substitutions
    = new Dictionary<Type, Func<PerformContext, object>>
    {
        { typeof (IJobCancellationToken), x => x.CancellationToken },
        { typeof (CancellationToken), x => x.CancellationToken.ShutdownToken },
        { typeof (PerformContext), x => x }
    };
```

このように定義されています。
つまり、`IJobCancellationToken`, `CancellationToken`, `PerformContext` 型の引数はすべて
`context` に基づく値に置換されるわけです。

ここでいう `context` とは `PerformContext` 型であり、
`PerformContext` 型の引数は `context` そのものが渡されるわけですね（ `x => x` より）

## 実装

今回の実装したコードは以下のとおりです。

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

これを実行すると、`Super heavy task` の後に JobId が出力されるはずです。

```
…略…
Super heavy task #5ea2bd72f7e04553001e545c
DummyAwesomeService#DoSomething start
Super heavy task #5ea2bd72f7e04553001e5456
DummyAwesomeService#DoSomething start
DummyAwesomeService#DoSomething end
Done: task #9
DummyAwesomeService#DoSomething end
Done: task #8
2020-04-24 07:20:49 [INFO]  (server.Filters.ReportPerformanceFilter) {"latency":11650,"duration":3010}
2020-04-24 07:20:49 [INFO]  (server.Filters.ReportPerformanceFilter) {"latency":1165
7,"duration":3014}
```

### JobIdが変わるタイミング

```csharp
public void Execute(string strText, PerformContext context) {
    Console.WriteLine($"Super heavy task #{strText} #{context.BackgroundJob.Id}");
    awesomeService.DoSomething();
    throw new Exception("something wrong!");
    Console.WriteLine($"Done: {strText}");
}
```

このように、一旦タスクの途中で例外を吐いて、故意に止めてみます。
するとHangfireで自動的に再実行されるのですが…

```
Super heavy task #task #0 #5ea2bdea9f055b342c65f067
…中略…
Super heavy task #task #0 #5ea2bdea9f055b342c65f067
```

と、`JobId` は変わっていないことがわかります。
`JobId` は Jobが生成されたタイミングで付与されるようです。

# 終わりに

今回はタスクに対して、実行時のコンテキストを付与する方法を書きました。

しかし今回の書き方では、
タスククラスに対して Hangfire への依存をもたせてしまいました。

これでは純粋に機能だけでテストしたい場合に困ります。
次回以降、DependencyInjection（かな？）を使用してそこの依存性を解消していきたいと思います。