(※内容薄いです...) 昨年 Blazor WebAssembly on .NET5 で作った "snow catch" ゲームを .NET6 に更新してみた

# "snow catch" ゲームとは

昨年 Qiita Advent Calendar 2020 への投稿ネタとして、"snow catch" というゲームを、Blazor WebAssembly を使って Web アプリとして作成しました。
("ゲーム" と言うにはあまりに質素ではあるのですが...)

"snow catch" ゲームの画面の様子は下図のとおりで、画面下の雪だるまをキーボードの左右の矢印キーで操作して、上から降ってくる雪を捕まえよう、っていう只それだけの Web アプリです。
時間制限もゲームオーバーもありませんw

![image](https://camo.qiitausercontent.com/372dd9ecaad06fbe305f1954b0227104550f5653/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f35383435302f63323565653234342d623332322d633639312d633565612d3166323138306531666631312e676966)

当時の記事は下記になります。

https://qiita.com/jsakamoto/items/8214211c1fd2ff91f46b

GitHub Pages に配置もしているので、下記 URL をブラウザで開けば実際に遊ぶこともできます。
なお、ブラウザで開けば即開始となります。

https://sample-by-jsakamoto.github.io/SnowCatch-on-BlazorWasm/

# "snow catch" ゲームを .NET 5 から .NET 6 へ移行する

さてそんな "snow catch" ゲームは、その当時の安定バージョンである .NET 5 上で開発されています。

そして先月 2021 年 11 月についに .NET 6 が公式リリースとなりましたので、これを機会に "snow catch" ゲームを、.NET 6 に移行してみました。

ただし、"snow catch" ゲームは**あまりに小規模**な Blazor WebAssembly アプリであるため、「.NET 5 から .NET 6 への移行のノウハウや Tips」**みたいな話はまったくなく**、さくっと移行完了してしまいました。😅

そういう意味で、記事タイトルには「**(※内容薄いです...)**」と但し書きさせていただいた次第です。

それでは以下に行なった手順を記していきます。

## 1. .NET 6 SDK をインストール

何はともあれ、.NET 6 の SDK が開発環境にインストールされていないと始まりません。

下記公式サイトから .NET 6 SDK を入手の上、開発環境にインストールしておきましょう。

https://dotnet.microsoft.com/download

なお、Windows OS をお使いで Visual Studio 2022 を「ASP.NET と Web 開発」ワークロードを含めてインストール済みの場合は、同時に .NET 6 SDK もインストールされているはずです。
その場合は、別途 .NET 6 SDK をダウンロード & インストールする必要はありません。

## 2. 対象フレームワークを .NET 6 に & NuGet パッケージの参照バージョンを最新に

まずは "snow catch" ゲームの C# プロジェクトファイル `SnowCatch.csproj` をエディタで編集し、対象フレームワークを .NET 6 に変更します。

```xml:SnowCatch.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
                 <!-- 👆 ここに "net5.0" と書かれていたところを、"net6.0" に書き換える -->
  </PropertyGroup>
  ...
```

さらに同プロジェクトファイルにて参照している NuGet パッケージのバージョンも、最新版を参照するよう、`Version="..."` のところを書き換えていきます。
とりわけ、"Microsoft.AspNetCore.～" 系の NuGet パッケージのバージョンが、"5.0.0" から "6.0.0" への書き換えとなるあたりが、.NET 6 への移行感がありますね。

```xml:SnowCatch.csproj
...
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
    <PackageReference Include="PublishSPAforGitHubPages.Build" Version="1.3.2" />
    <PackageReference Include="System.Drawing.Primitives" Version="4.3.0" />
    <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
    <PackageReference Include="Toolbelt.Blazor.HotKeys" Version="12.0.0" />
  </ItemGroup>
</Project>
```

Windows OS 上で Visual Studio 2022 をお使いの場合は、パッケージ管理 GUI ウィンドウからパッケージの更新を実施してもよいでしょう。

以上で `dotnet run` でビルド & 実行すれば、**ちゃんと .NET 6 化された "snow catch" ゲーム**が起動しました!

## 3. "暗黙的な global using ディレクティブ" を有効にしてみる

さて、"snow catch" ゲームの .NET 6 化、というだけなら以上でおわりとなります。😁
ですが、さすがにそれだけではあんまりなので、もう少し新機能を使うように変更していきます。

.NET6 というか、C#10 の機能とも関連するわけですが、**"暗黙的な global using ディレクティブ"** を有効にしてみます。

"暗黙的な global using ディレクティブ" を有効とは、すなわち、`System` や `System.Linq` などなど、**「大抵は使うであろう」という名前空間を、個々の C# ソースコード中で明示的に using を記載することなく、**プロジェクト中のすべての C# ソースコードについて using したのと同じようにコンパイルする構成です。
具体的にどういった名前空間が using 済みとなるのかは SDK によって定義されています。

"暗黙的な global using ディレクティブ" を有効にするには、プロジェクトファイル中にてその旨の設定を追記すればよいです。

```xml:SnowCatch.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>

    <!-- 👇 下記を追記 -->
    <ImplicitUsings>enable</ImplicitUsings>

  </PropertyGroup>
  ...
```

公式ドキュメントは下記にあります。

https://docs.microsoft.com/ja-jp/dotnet/core/project-sdk/msbuild-props#implicitusings

以上の設定を済ませたらあとは、"snow catch" 内の C# コードを巡回して、明示的な記載が不要になった using ディレクトリを削除していって完成です。

例えば、`Program.cs` の冒頭は、昨年実装の時点でも不要だった using を削除していなかったこともあり、下記の状態から、

```csharp:Program.cs
using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Toolbelt.Blazor.Extensions.DependencyInjection;
```

下記のたった2行へと激減しました。

```csharp:Program.cs
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Toolbelt.Blazor.Extensions.DependencyInjection;
```

ところで、実用的な C# プログラムにおいては、相当数の C# ソースコードファイルがプロジェクトに含まれているはずです。
そのような場合に、**C# ソースコードファイルをひとつずつ手作業で開いては書き換えるのはナンセンス**です。
そのような場合は、**`dotnet format` コマンドを使う**とか、Windows OS 上で Visual Studio 2022 を使っているなら Visual Studio の機能を使うとかの方法で、**一括で C# ソースコードを改訂**するのがよいでしょう。

なお、"snow catch" ゲームでは、実質的な C# ソースファイルは 2つしかありません。
そのため、手作業でさくっと変更作業が終わってしまいました。

## ただし、一点だけ、問題が発生

さて、"暗黙的な global using ディレクティブ" を有効にしたところ、一点だけ、下記ビルドエラーが発生しました。

```
GameContext.cs(12,12,12,17): error CS0104: 
'Timer' is an ambiguous reference between 'System.Timers.Timer' and 'System.Threading.Timer'
```

このビルドエラーの原因ですが、まず、"暗黙的な global using ディレクティブ" が有効になったため、明示的な記載がなくとも `using System.Threading` したのと同じ状態となっています。
その結果、C# ソースコード `GameContext.cs` 中で記述していた `Timer` クラスが、`System.Timers` 名前空間の `Timers` クラスなのか、`System.Threading` 名前空間の `Timer` クラスなのか、区別がつかなくなってしまった、ということでした。

```csharp:GameContext.cs
using System.Timers;
// using System.Threading; // 👈 .csファイル中に記載なくとも、この using があるのと同等の状態
  ...
  public Timer GameLoopTimer = new(interval: 30);
       // 👆 System.Timers.Timer のことなのか System.Threading.Timer のことなのか
       //     区別がつかなくなってしまった💦
  ...
```


このビルドエラーについては、C# ソースコード `GameContext.cs` を書き換え、名前空間込みで `System.Timers` 名前空間の `Timer` クラスのことであると明記するようにして解決しました。

```csharp:GameContext.cs
  ...
  public System.Timers.Timer GameLoopTimer = new(interval: 30);
         // 👆 名前空間込みで型名を記述
  ...
```



## 4. `Program.cs` を "最上位レベルのステートメント" を使ったスタイルに書き換える

もうひといき、変更を加えてみることにします。
`Program.cs` を **"最上位レベルのステートメント"** を使ったスタイルに書き換えることにしましょう。

"最上位レベルのステートメント" とは、**プログラムの開始地点 (エントリポイント) を、クラスや静的メソッド (Main) で囲むことなく記述**できる構文です (公式ドキュメントは下記にあります)。

https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-9#top-level-statements

実は "最上位レベルのステートメント" 構文は、C# 9 の時代から使えていました。
しかしながら、Blazor WebAssembly プログラミングでは .NET 6 から "最上位レベルのステートメント" を使ったスタイルでの記述が可能になりました。
ということで、これを機会に "snow catch" ゲームの `Program.cs` に適用してみます。

変更前は下記のとおりであった `Program.cs` が、

```csharp:Program.cs
...
namespace SnowCatch
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddHotKeys();

            await builder.Build().RunAsync();
        }
    }
}
```

"最上位レベルのステートメント" スタイルに書き直すことで、名前空間・クラス・Main 静的メソッドの囲いがなくなり、インデントも不要になって、下記のとおりスッキリした見た目になりました。

```csharp:Program.cs
...
using SnowCatch;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddHotKeys();

await builder.Build().RunAsync();
```

# 以上!

以上で "snow catch" ゲームの .NET 6 化および C# の最新構文への書き換えは完了です。

"snow catch" ゲームの GitHub リポジトリ上では、`net6` ブランチ (下記リンク先) にて、移行・変更の様子を確認できます。

https://github.com/sample-by-jsakamoto/SnowCatch-on-BlazorWasm/commits/net6

"snow catch" ゲームの規模では、.NET 6 の新機能の出番もなく、逆に .NET 5 と .NET 6 の間の非互換を踏むようなこともなく、平穏に移行が終わってしまいました。
せいぜい、C# の最新構文を使って書き換えたことで、ソースコードがスッキリする程度でしたでしょうか。

そうそう、あと、**.NET 6 は長期サポート版 (LTS)** ですから、**向こう3年間はサポートが約束**されていますので、その点は安心ですね!

以上!