Start Debugging

.NET 8 から .NET 11 への移行: 完全チェックリスト

.NET 8 LTS から .NET 11 LTS へのバージョン固定の移行チェックリストです。SDK のインストール、csproj の target framework、ASP.NET Core / EF Core / System.Text.Json の破壊的変更、C# 14 のオーバーロード解決の変化、ロールバックの注意点を扱います。

LTS を 2 回まとめて飛び越えるのは、この 10 年でほとんどのチームが行う中で最も安い .NET アップグレードです。.NET 8 は 2026 年 11 月に標準サポートを抜け、.NET 11 が現在の LTS で、その間の道のりは 3 セットの破壊的変更(.NET 9, 10, 11)と 3 つの C# 言語バージョン(C# 13, 14、加えて C# 12 はすでに 8 で提供済み)を通ります。小さなサービスなら集中した週末で十分です。EF Core、カスタムミドルウェア、いくつかの source generator を持つ中規模のモノリスは通常 3 ~ 5 日かかります。BinaryFormatter を固定していたり、System.Web.HttpContext のシムに依存したり、in-process な Azure Functions を動かしているコードベースはもっとかかり、その痛みが最初に現れます。

この投稿では net8.0 をソース、net11.0 をターゲットとして使います。コードブロックごとにバージョンを明示的に固定するため、いくつかのパッチリリースを経ても手順は再現可能なままです。

なぜ今移行するのか

何が壊れるか

領域変更重大度
lock(object)新しい System.Threading.Lock 型は採用時に monitor のセマンティクスを変える
BinaryFormatter.NET 9 で完全に削除。opt-in スイッチなし
System.Text.JsonJsonObject ラウンドトリップのデフォルト JsonNumberHandling が .NET 10 で変化
EF Core クエリパイプラインプリミティブコレクションのトランスレーションが EF Core 10 で変わり、一部 LINQ が例外を投げる
ASP.NET Core ミドルウェアUseExceptionHandler のオーバーロードシグネチャが .NET 10 でシフト
Native AOT のトリム警告いくつかの System.Reflection.Emit パスが新たに IL2026 警告を出すように
C# 14 のオーバーロード解決Span オーバーロードが配列オーバーロードに勝つようになる(曖昧な場合)
IWebHostBuilder8 ですでに非推奨、11 で削除。WebApplication.CreateBuilder に移行
dotnet ef ツールメジャーバージョンアップが必要 (dotnet tool update --global dotnet-ef --version 11.*)
Azure Functionsin-process モデルは削除。isolated worker が必須

完全な公式リストは .NET 11 の破壊的変更ドキュメント にあります。.csproj に触れる前に最初から最後まで読んでください。

プリフライトチェックリスト

target framework を変更する前にこれを実行してください。

  1. すべての開発マシンと CI ランナーに .NET 11 SDK をインストールします。dotnet --list-sdks で確認し、11.0.x が表示されることを確かめます。SDK は side-by-side なので .NET 8 は引き続き動作します。
  2. CI が静かにロールフォワードしないように global.json で SDK を固定します。
    // global.json, repo root
    {
      "sdk": {
        "version": "11.0.100",
        "rollForward": "latestFeature"
      }
    }
  3. ベースラインを取ります。.NET 8 上で dotnet test を実行し、結果を保存します。アップグレード後の最初の赤を明確にするため、開始前にクリーンな緑が欲しいのです。
  4. 本番ランタイムのスナップショットを取ります。ライブホストから dotnet --info をダンプします。8.0.0 より古いランタイムにリンクしているものがあれば(古い self-contained 公開、サードパーティのプラグイン)、今のうちに見つけてください。
  5. dotnet list package --outdated --include-transitive で NuGet パッケージを棚卸しします。Microsoft.*8.0.x に固定しているものはメジャーバンプが必要、7.* 以前に固定しているものはレッドフラグです。
  6. 移行用にブランチを切ります。論理ステップごとの 1 PR は、巨大な一発 PR より戻しやすいです。

移行手順

  1. target framework を上げる。 すべての .csproj を開き、TargetFramework(または TargetFrameworks)の値を変更します。dotnet build で確認し、最初に出る compile エラー群を移行の本当のスコープとして扱います。

    <!-- src/MyApi.csproj, .NET 11 -->
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>net11.0</TargetFramework>
        <LangVersion>14.0</LangVersion>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    </Project>

    検証: dotnet build が少なくとも leaf プロジェクトで 0 終了するか、認識できるエラーで失敗する。

  2. すべての Microsoft.* NuGet パッケージを 11.x 系に更新する。 Directory.Packages.props を盲目的に触るのではなく、プロジェクトごとに dotnet add package で 1 つのバッチとして行います。ランタイム、ASP.NET Core、EF Core、Microsoft.Extensions.* パッケージは SDK と歩調を合わせてバージョンが上がります。

    # .NET 11
    dotnet add package Microsoft.AspNetCore.OpenApi --version 11.0.0
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 11.0.0
    dotnet add package Microsoft.Extensions.Hosting --version 11.0.0

    検証: dotnet restore が成功し、dotnet list packageMicrosoft.* 名前空間下に 8.0.x を残していない。

  3. BinaryFormatter の利用を取り除く。 コードベースが BinaryFormatter で何かをシリアライズしているなら、今置き換えてください。JSON ワイヤフォーマットが必要かバイナリが必要かに応じて、System.Text.Json、MessagePack、protobuf-net が通常の置き換えです。.NET 9 以降には互換フラグはなく、型自体がなくなりました。

    検証: grep -r "BinaryFormatter" src/ が何も返さない。ストレージから古い BinaryFormatter ブロブを読む必要があるなら、.NET 8 環境を落とす前に、それらを変換するための使い捨ての .NET 8 移行ツールを書いてください。

  4. IWebHostBuilderWebApplication.CreateBuilder に置き換える。 古い generic-host シムは .NET 6 で非推奨となり、.NET 11 で削除されました。まだ Host.CreateDefaultBuilder().ConfigureWebHostDefaults(...) を呼び出している Program.cs はコンパイルできません。

    // Program.cs, .NET 11, C# 14
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddOpenApi();
    builder.Services.AddDbContext<AppDb>(o =>
        o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
    
    var app = builder.Build();
    app.MapOpenApi();
    app.MapControllers();
    app.Run();

    検証: アプリが dotnet run で起動し、/openapi/v1.json エンドポイントが HTTP 200 で応答する。

  5. System.Text.Json の動作変更を監査する。 JsonObject の数値ラウンドトリップのデフォルト処理は .NET 10 で変わり、整数を再シリアライズしても精度を失わなくなりました。また、ポリモーフィックなデシリアライザはデフォルトで未知の discriminator に対してより厳格になりました。公開 API 契約を保守しているなら、契約テストを走らせて失敗を注意深く読んでください。契約自体は変わっていないことが多いのですが、以前は静かに通っていたミスマッチが今は例外を投げます。コンパニオン投稿の JSON value could not be converted to System.DateTime の修正 が最も一般的な変換失敗モードを扱っています。

    検証: フィクスチャ JSON ファイルに対してシリアライズを動かしているプロジェクトすべてで dotnet test がクリーンに通る。

  6. プリミティブコレクションを使う EF Core クエリを移行する。 EF Core 10 では List<int>.Contains(x) の翻訳が作り直され、パラメータ化されたコレクションが IN 句に展開されず単一の SQL パラメータを生成するようになりました。これにより plan-cache 肥大の問題は解決しましたが、Contains をサーバ評価される他の式と組み合わせていた小さなクエリ群が壊れました。すべての EF Core 統合テストを再実行し、InvalidOperationException: The LINQ expression ... could not be translated を投げるようになったクエリを点検してください。脱出ハッチは、結合前にコレクションを .ToList() でマテリアライズすることです。

    検証: DbSet<T> に対する素の LINQ を動かしている統合テストがすべて通る。代表的なクエリで LogTo(Console.WriteLine, LogLevel.Information) を使って生成された SQL を抜き取り検査する。

  7. System.Threading.Lock を一括置換ではなく選択的に採用する。 private readonly object _gate = new();private readonly System.Threading.Lock _gate = new(); に置き換えるのはほとんどの場合正しいのですが、同一スレッドからの再入が観測可能かどうかを変えてしまいます。先にコードパスを歩いてください。より深いトレードオフ比較は lock vs Monitor vs SemaphoreSlim vs System.Threading.Lock にあります。

    検証: コードレビューが変更されたすべての lock(...) サイトを明示的にカバーする。テストスイートに機能変化なし。

  8. trim と AOT のアナライザを再実行する。 プロジェクトが <PublishAot>true</PublishAot> または <TrimMode>full</TrimMode> を設定しているなら、.NET 8 では静かだった System.Reflection.Emit 経路のあたりで .NET 11 は新しい警告を出します。修正は通常 [DynamicallyAccessedMembers] アノテーションを追加するか、JSON の source generator を登録することです。Native AOT vs ReadyToRun vs JIT の比較 が各モデルがコストに見合うタイミングを扱っています。

    検証: leaf プロジェクトで dotnet publish -c ReleaseIL2026IL3050 の警告をゼロで出す。生成された native バイナリがローカルで起動する。

  9. C# 14 のオーバーロード解決の驚きを調整する。 C# 14 は解決規則を変え、ReadOnlySpan<T> を受けるオーバーロードが T[] を受けるオーバーロードより優先されるようになりました(両方適用可能な場合)。ほとんどのコードは影響を受けません。壊れるのは通常、モック、流暢な assertion ライブラリ、配列オーバーロードが勝つ前提で書かれたカスタムの拡張メソッドです。コンパイラは明確な診断を出します。修正はたいてい cast です。C# 14 のオーバーロード解決の破壊的変更(span) が診断と cast パターンを案内します。

    検証: <TreatWarningsAsErrors>true</TreatWarningsAsErrors>dotnet build が警告ゼロ。

  10. CI ランナーイメージを更新する。 GitHub Actions の actions/setup-dotnetdotnet-version11.0.x に上げ、Dockerfile のベースイメージを mcr.microsoft.com/dotnet/sdk:11.0mcr.microsoft.com/dotnet/aspnet:11.0 に更新し、.NET 8 SDK イメージへのピンを外します。セルフホストランナーは CI を通す前に手で SDK をインストールする必要があります。

    検証: feature ブランチでパイプライン実行が publish ステップを含めて端から端まで緑。

検証(スモークチェックリスト)

上記の手順の後、移行 PR をマージする前に、アプリは次のリストのすべての行を通過するはずです。

どれか 1 つでも落ちたら止めてください。部分的にしか移行されていないコードベースをマージしないこと。

ロールバック

この移行は、.NET 11 下で書き込みを受ける最初の本番デプロイまで可逆です。それまでは global.jsonTargetFramework、NuGet のバンプを 1 コミットで戻してください。.NET 11 で最初の本番書き込みを終えた後はロールバックは技術的に可能ですが、ほとんど割に合いません。EF Core 11 のトランスレータの下で行ったかもしれないスキーマ変更、新しいデフォルトでシリアライズされた JSON 出力、System.Threading.Lock の採用はそれぞれ別の検討が必要です。前進して直す計画にしてください。

ぶつかった落とし穴

関連

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る