🌥️

Blazor Server でブラウザを閉じても処理を継続する

に公開

はじめに

Blazor Web Appのサーバ側で、時間のかかる処理を行っている間、ブラウザを閉じたりしないようにUIをブロックしていたのですが、開いたままにしておくのも何かと不便なので、バックグラウンドサービスにしてみました。

前提

  • .NET 8

実装

タスク

バックグラウンド処理のモデルを定義します。
https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Services/UpdateBookTask.cs#L1-L9

更新対象の「レコードID」と「完全/部分」種別を保持します。

キュー

タスクを共有するFIFOを定義します。
https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Services/UpdateBookQueueService.cs#L1-L29

同じ処理が既に待機中なら登録しないようにしたいのですが、Channelはスキャンに向かないし、DBはロックしたくないので、別途ConcurrentDictionaryで待機列を管理しています。
待機列に含まれるか確認するContainsと、処理完了時に待機列から除去するCompletedが用意されています。

サービス

実際の処理内容を定義します。
https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Services/UpdateBookService.cs#L1-L63
StartAsyncは、アプリの起動時に呼ばれ、別スレッドでバックグラウンド処理を開始して、自身はすぐ終わります。
バックグラウンド処理でスコープサービスを利用するために、まずスコープを生成し、その中でサービスを注入しています。
そして、キューからタスクを取り出して順に処理UpdateBookFromSiteAsyncします。
タスクがなくなれば、新しいタスクがキューに登録されるまで待機します。
個々のタスクが完了する際は、待機列(辞書)から自身を除去します。
アプリのシャットダウン時にはStopAsyncが呼ばれます。

DIコンテナへの登録

起動時にサービスを登録します。
https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Program.cs#L74-L75

Program.cs
builder.Services.AddSingleton<UpdateBookQueueService> ();
builder.Services.AddHostedService<UpdateBookService> ();

これらはアプリ全体で共有するので、キューUpdateBookQueueServiceはシングルトンです。
サービスUpdateBookServiceIHostedServiceを実装したシングルトンとしてホストされ、アプリのライフサイクルに統合されます。

使用例

キューの注入

https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Components/Pages/ItemListBase.cs#L31

着目中のレコードがキューに登録されているか判別

https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Components/Pages/ItemListBase.cs#L34

キューにタスクを登録 (処理の追加)

https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Components/Pages/Issue.razor.cs#L173

Issue.razor.cs
await UpdateBookQueue.EnqueueAsync (new UpdateBookTask { Id = SelectedItem.Id, FullUpdate = fullUpdate, });

個々のレコードが処理(待機)中か否かを表示

https://github.com/tetr4lab/Novels/blob/7882b4596d9c871ff529522cbf041b5d48e03eac/Novels/Novels/Components/Pages/Books.razor#L27-L31

おわりに

最後までお読みいただきありがとうございました。
サンプルコードの全体は、以下のリポジトリにあります。

https://github.com/tetr4lab/Novels

何かお気づきの際は、是非ご指摘ください。
あるいは、「それでも解らない」、「自分はこう捉えている」などといった、ご意見、ご感想も歓迎いたします。

Discussion