Blazor SPA (v.0.5.x) で、ローカルタイムゾーンを解決する方法

# その前に: "Blazor" とは?

"Blazor" とは、シングルページ Web アプリケーション (いわゆる "SPA") フレームワークです。

Blazor は、C# コードおよび .NET Standard 2.0 のネイティブアセンブリ (.dll) を、.NET サーバープロセスを使用せずに、モダン Web ブラウザ上で実行します。

もし Blazor について初めての方は、手前味噌ながら下記ポストをご覧頂くとよいかと思います。

**[C# で Single Page Web Application が書ける Blazor が凄かった件](https://qiita.com/jsakamoto/items/20d4893f6c8cdb0356f6)**

## 問題: Blazor SPA は一切のタイムゾーン情報を持っていない ...!

Blazor はとりわけ C# 開発者にとっては素晴らしいフレームワークだと自分は感じています。

しかしながら、Blazor はまだ実験的プロジェクトの段階です。

最近、私は、Blazor がまだ実験的プロジェクトであることを象徴するような出来事を見つけました。

驚くべき事に、**Blazor アプリケーション上では、システムタイムゾーン情報が、なんと 0 件なのです!**

そのため、OS やブラウザのロケール設定に依らず、`"TimeZoneInfo.Local"` が返すタイムゾーンは常に UTC ですし、`"DateTime.Now"` が返す時刻は常に UTC 時刻です。

![fig.1](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/.assets/fig-2018-09-03-001.png?raw=true)

もし、ローカル時刻やその他のタイムゾーン情報にアクセスする必要が発生したら、どうしたらよいのでしょうか!?

## 解決策: "Toolbelt.Blazor.TimeZoneKit" NuGet パッケージの使用

この問題は、次の手順で ["Toolbelt.Blazor.TimeZoneKit" NuGet パッケージ](https://www.nuget.org/packages/Toolbelt.Blazor.TimeZoneKit/) を Blazor アプリケーションプロジェクトに追加することで解決できます。

**ステップ.1** - 例えば次のように dotnet CLI を使うなどして、この NuGet パッケージをプロジェクトに追加します。

```shell
> dotnet add package Toolbelt.Blazor.TimeZoneKit
```

**ステップ.2** - このパッケージで提供されている `"UseLocalTimeZone()"` 拡張メソッドを、startup クラスの `"Configure()"` メソッド内で実行します。

```csharp
...
using Toolbelt.Blazor.TimeZoneKit;

public class Startup
{
    ...
    public void Configure(IBlazorApplicationBuilder app)
    {
        app.UseLocalTimeZone(); // ← ココ
    ...
```

これで解決です。

![fig.2](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/.assets/fig-2018-09-03-002.png?raw=true)

# どのようにしてシステムタイムゾーン情報をセットアップしているか?

特別な仕掛けはありません。

"Toolbelt.Blazor.TimeZoneKit" は、すべての .NET システムタイムゾーン情報を、そのアセンブリファイル (.dll) 内に抱えているだけです。
(参照: ["TimeZoneKit.CreateSystemTimeZones.cs"](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/Toolbelt.Blazor.TimeZoneKit/TimeZoneKit.CreateSystemTimeZones.cs#L16))

`"UseLocalTimeZone()"` 拡張メソッドは、.NET のリフレクション API によって、`"TimeZoneInfo"` クラスのプライベートメンバーを上書きすることで、アプリケーションランタイムのシステムタイムゾーン情報をセットアップします。

# どのようにしてブラウザのローカルタイムゾーンを取得しているか?

最近のモダン Web ブラウザは、`"Intl.DateTimeFormat().resolvedOptions().timeZone"` JavaScript コードをサポートしています。

この JavaScript コードは、ブラウザのローカルタイムゾーン名を返します。

Blazor は、C# コードと JavaScript コードとの相互運用機能を提供しているので、`"UseLocalTimeZone()"` 拡張メソッド内から上記の JavaScript コードを呼び出して、ローカルタイムゾーン名を取得することができます。

![fig.3](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/.assets/fig-2018-09-03-003.png?raw=true)

しかしながら、C# コードからの JavaScript コード呼び出しは非同期です。

`"UseLocalTimeZone()"` 拡張メソッドが、その実行内でローカルタイムゾーンのセットアップを同期的に完了できなければ、面倒なバグを引き起こすことになります。

![fig.4](https://raw.githubusercontent.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/master/.assets/fig-2018-09-04-001.png)

かといって、非同期 Task の結果を同期待ちすることは、デッドロックを引き起こすため、できません。

![fig.5](https://raw.githubusercontent.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/master/.assets/fig-2018-09-04-002.png)

この問題を解決するため、`"UseLocalTimeZone()"` 拡張メソッドは、"C# コードをコールバックする" JavaScript コードを呼び出します。

JavaScript コードから C# コードを呼び出す機能は同期的に実行されるため、ローカルタイムゾーンのセットアップ処理は、`"UseLocalTimeZone()"` 拡張メソッドが終わる前に完了するようになります。

![fig.6](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/.assets/fig-2018-09-04-003.png?raw=true)

# どのようにして IANA タイムゾーン名を .NET タイムゾーン ID に変換するのか?

JavaScript コード `"Intl.DateTimeFormat().resolvedOptions().timeZone"` はローカルタイムゾーンの "名前" を返しますが、これは .NET タイムゾーン ID とは違います。

JavaScript におけるローカルタイムゾーン名は、"IANA" タイムゾーン名と呼ばれます。

IANA タイムゾーン名を、.NET タイムゾーン ID に変換するために、"Toolbelt.Blazor.TimeZoneKit" は、IANA タイムゾーン名から .NET タイムゾーン ID を表引きするマッピング情報を、そのアセンブリファイル (.dll) 内に抱えています。
(参照: ["TimeZoneKit.IANAtoTZIdMap.cs"](https://github.com/jsakamoto/Toolbelt.Blazor.TimeZoneKit/blob/master/Toolbelt.Blazor.TimeZoneKit/TimeZoneKit.IANAtoTZIdMap.cs#L5))


# 補足

- **警告** - このライブラリは、"Reflection" .NET API を介して System.TimeZoneInfo クラスのプライベートメンバーにアクセスするため、.NET ランタイム (mono.wasm) の将来のバージョンアップによって壊れる可能性があります。
- 私のテストケースでは、このライブラリは、Blazor アプリケーションのコンテンツファイルサイズを 154 KB 増加させました。(gzip 圧縮が適用された転送サイズは 20KB 増加。)

# まとめ

Blazor の将来リリースでは、ローカルタイムゾーンがサポートされることとは思います。

参考: [Blazor - Issue #1166 "DateTime always displays as UTC"](https://github.com/aspnet/Blazor/issues/1166)

それまでの間、"Toolbelt.Blazor.TimeZoneKit" NuGet パッケージはローカルタイムゾーンの取り扱いの助けとなることでしょう。

Happy coding with Blazor :)