こんにちは!.NET 11 の第6版プレビューがリリースされました。11月の正式版が近づくにつれて、このバージョンの新機能もますます“使える”ものになってきています。
先に結論を言うと、今回の最大の見どころは「非同期検証」の一式がついに揃ったこと、union 型がすぐ使えるようになったこと、そして CSRF 防御が既定で有効になったことです。以下、順番に見ていきましょう。
1. ASP.NET Core: Web 開発関連の更新が最も多い
1. 非同期検証がついに登場
これまで DataAnnotations を使った検証には、長年の課題がありました。検証ルールが同期処理にしか対応していなかったのです。検証時にデータベースを確認したい場合(たとえば「このメールアドレスは既に登録済みか」など)、スレッドをブロックして待つか、検証の仕組みを回避して自前で実装するしかありませんでした。
これで解決です。AsyncValidationAttribute を継承し、IsValidAsync メソッドを実装すれば、検証の中で安心して await できます。
public sealed class UniqueEmailAttribute : AsyncValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext context) =>
throw new InvalidOperationException("IsValidAsync を使って検証してください");
protected override async Task<ValidationResult?> IsValidAsync(
object? value, ValidationContext context, CancellationToken cancellationToken)
{
var users = context.GetRequiredService<IUserService>();
if (value is string email && await users.EmailExistsAsync(email, cancellationToken))
{
return new ValidationResult("このメールアドレスは既に登録されています");
}
return ValidationResult.Success;
}
}
あとは通常の検証属性と同じように、プロパティに [UniqueEmail] を付けるだけです。
検証ロジックが複数のプロパティにまたがる場合(たとえば「この日付はまだ空いているか」など)は、モデル側で IAsyncValidatableObject インターフェースを実装できます。Minimal API では AddValidation() を登録するだけで、フレームワークがエンドポイントに入る前にこれらの非同期検証を自動で実行してくれます。
さらにフレームワークは賢く、並列実行できる検証は並列で行い、コレクション要素も並列で検証されるため、性能面でも不利になりません。
2. CSRF 防御: 既定で有効、設定不要
この変更は全員が注意すべきです。WebApplication.CreateBuilder で作成されたアプリでは、ブラウザから送られる Sec-Fetch-Site と Origin リクエストヘッダーに基づいて、安全でないクロスサイトリクエストが自動的に拒否されるようになりました。
わかりやすく言うと、以前は CSRF 攻撃を防ぐために Antiforgery Token を自分で設定する必要がありましたが、今ではフレームワークが軽量な防御層を標準搭載しています。Minimal API、MVC、Razor Pages、Blazor のすべてが対象で、コードを1行も書く必要はありません。
通常の同一オリジンのリクエスト、ユーザーによる能動的なページ遷移、ブラウザ以外のクライアント(たとえば自分のアプリからの API 呼び出し)は影響を受けず、ブラウザから発信された疑わしいクロスサイトリクエストだけをブロックします。
特定のエンドポイントを除外したい場合は、Minimal API なら .DisableAntiforgery()、MVC なら [IgnoreAntiforgeryToken] を使います。全体で無効化したい場合は、DisableCsrfProtection 設定項目を変更すればOKです。
3. Blazor の仮想リストが「指定項目へジャンプ」できるように
Virtualize コンポーネントを使ったことがある人なら、長大なリストの描画に向いているのは知っているはずです。ただ以前は、特定の項目へ直接スクロールすることができませんでした。今回、次の2つの API が追加されました。
-
InitialIndex: リストの初回描画時に直接 N 番目の項目へ定位し、最初の項目が一瞬表示されてから飛ぶ、という動きを避けられます -
ScrollToIndexAsync(int itemIndex): いつでも任意の項目へスクロールできます。たとえば「トップへ戻る」ボタンの実装に使えます
<Virtualize TItem="Product" Items="products" InitialIndex="500" @ref="list">
<div class="product">@context.Name</div>
</Virtualize>
<button @onclick="GoToTop">トップへ戻る</button>
@code {
private async Task GoToTop() => await list.ScrollToIndexAsync(0);
}
細かい点もよく考えられています。インデックスが範囲外なら自動的に有効範囲へ収束し、スクロール中にユーザーが手動スクロールした場合は、ユーザーの操作が優先されます。
4. サーバー側で Blazor クライアントの挙動を設定可能に
以前は、Blazor クライアントの起動時の挙動(ログレベル、再接続ポリシーなど)を調整したい場合、Blazor.start の JavaScript を手書きする必要がありました。これからは C# で直接設定できます。
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.WithBrowserOptions(options =>
{
options.LogLevel = LogLevel.Warning;
options.Server.ReconnectionMaxRetries = 10;
options.Server.ReconnectionRetryInterval = TimeSpan.FromSeconds(1.5);
});
サーバー側が設定をページにシリアライズし、ブラウザ側の Blazor スクリプトが自動的に適用します。Server、WebAssembly、Auto の3つのレンダリングモードすべてに対応しています。
5. [ShortCircuit] 属性: エンドポイントを“近道”させる
健康チェックや robots.txt のように、認証や CORS といったミドルウェアが不要なエンドポイントもあります。新しい [ShortCircuit] 属性を使うと、こうしたエンドポイントはルートがマッチした時点で直接実行され、その後のミドルウェアパイプライン全体をスキップできるため、不要なオーバーヘッドを削減できます。
app.MapGet("/health", [ShortCircuit] () => "Healthy");
MVC コントローラーでも使え、ステータスコード付きで [ShortCircuit(404)] のように指定することもできます。この機能はコミュニティ提供です。素晴らしいですね。
6. SignalR の実用的な更新が2つ
トークン期限切れで切断されなくなりました。以前は、SignalR 接続で使う Bearer Token が期限切れになると、接続が切れていました。これからはサーバー側で認証更新を有効にでき、.NET クライアントはトークンの期限が近づくと自動で再認証し、接続を維持できます。
app.MapHub<ChatHub>("/chat", options =>
{
options.EnableAuthenticationRefresh = true;
});
現時点では .NET クライアントが対応済みで、JS クライアントは今後対応予定です。
また、通常呼び出しでもキャンセルできるようになりました。以前はストリーミング呼び出しのみキャンセル可能でしたが、今では InvokeAsync に CancellationToken を渡してキャンセルすると、サーバー側の Hub メソッド内の CancellationToken も発火するため、長時間処理を適切に中断できます。
2. C# 言語: 2つの更新、どちらも“新機能の補完”に関するもの
1. 拡張インデクサー
.NET 10 で導入された extension ブロックは、すでに拡張メソッドと拡張プロパティをサポートしています。今回、インデクサーも追加されました。つまり、他人の型に this[...] アクセスを追加できます。
IReadOnlyList<string> log = ["start", "work", "done"];
Console.WriteLine(log[^1]); // done、末尾から1つ目
public static class ReadOnlyListExtensions
{
extension<T>(IReadOnlyList<T> list)
{
public T this[Index index] => list[index.GetOffset(list.Count)];
}
}
上の例では、IReadOnlyList<T> に ^1 のような末尾からのインデックス機能を追加しています。本来は this[int] しかありません。ルールは他の拡張メンバーと同じで、型自身が持つインデクサーが優先され、見つからない場合に拡張のものが使われます。現在はプレビュー機能なので、<LangVersion>preview</LangVersion> が必要です。
2. union 型: 面倒なボイラープレートを書かなくてよくなる
union(ユニオン型)は C# 15 の大きなプレビュー機能で、簡単に言えば「1つの値が、複数の型のうちどれか1つである」ことを表します。
public record class Dog(string Name);
public record class Cat(int Lives);
public union Pet(Dog, Cat); // Pet は Dog か Cat のどちらか
static string Describe(Pet pet) => pet switch
{
Dog(var name) => $"犬です: {name}",
Cat(var lives) => $"猫です: {lives} 生活"
};
Preview 5 ではコンパイラ対応の型(UnionAttribute、IUnion)を自分で用意する必要がありましたが、Preview 6 ではそれらがフレームワークに直接組み込まれ、union を1行で宣言できるようになりました。
しかもエコシステム全体が対応しています。
-
System.Text.Jsonが union のシリアライズをネイティブサポートし、現在保持している値をそのまま出力します - ASP.NET Core では union をリクエストボディや戻り値に使え、Minimal API、MVC、SignalR、Blazor がすべて対応します
- OpenAPI ドキュメントでは union が
anyOfとして表現されます
つまり、app.MapGet("/pets/{id}", Pet (int id) => ...) のような書き方がそのまま動きます。なお、現時点では JSON シナリオのみ対応で、クエリ文字列、ルート、フォームからの union バインドはまだできません。
3. 基盤ライブラリ: 小さくても美しい API 群
1. 4つの新しい Stream アダプター
以前は、文字列やメモリブロックを Stream のみを受け取る API に渡したいとき、いったん MemoryStream にコピーする必要がありました。今回、コピー不要のラッパーが追加されました。
-
StringStream: 文字列をそのままストリームとして読み込む -
ReadOnlyMemoryStream:ReadOnlyMemory<byte>を読み取り専用ストリームとして包む -
WritableMemoryStream: 書き込み可能なMemory<byte>を固定長ストリームとして包む -
ReadOnlySequenceStream:ReadOnlySequence<byte>(Pipelines でよく使われる)をストリームとして扱う。連続メモリにまとめる必要はありません
// 文字列をそのままストリーム化し、byte[] への変換を不要にする
using Stream config = new StringStream(yamlText, Encoding.UTF8);
var settings = ParseConfiguration(config);
2. DataAnnotations 非同期検証の“土台”
先ほど ASP.NET Core の項目で触れた非同期検証の基盤は、今回の基盤ライブラリに追加された AsyncValidationAttribute、IAsyncValidatableObject、および Validator.ValidateObjectAsync などの新しいメソッド群です。
Microsoft.Extensions.Options も同時に対応し、設定オプションの非同期検証が可能になりました。さらに IAsyncStartupValidator による起動時検証もサポートされます。たとえば、ある設定項目がネットワーク確認を必要とする場合、設定ミスがあればアプリ起動時に即座にエラーを出して早期失敗できます。
3. ルールで Activity トレースを設定
以前は、どの Activity をトレースするか、どれを無効にするかを制御するために、たくさんの ActivityListener を自分で生成する必要がありました。これからは AddTracing 一発で済み、設定ファイルからルールを読み込んで、再デプロイなしでトレース範囲を調整できます。
builder.Services.AddTracing(tracing =>
{
tracing.EnableTracing(sourceName: "MyCompany.Orders");
tracing.DisableTracing(sourceName: "MyCompany.Orders", operationName: "HealthCheck");
});
4. プロセス制御がより細かく
-
ProcessStartInfo.StartSuspended: Windows でプロセスを中断状態で起動し、準備作業(たとえばデバッガーのアタッチ)を終えた後にSafeHandle.Resume()で再開できます -
Process.TryGetProcessById: プロセスが存在しない場合、例外を投げずにfalseを返します - ベクター型(
Vector128<T>など)に、Zip、Unzip、Reverseなどの跨 lane 操作 API が追加されました。画像処理や音声 DSP を扱う人には朗報です
4. ランタイム: 見えないところで静かに高速化
ランタイムの更新は、私たちにとって基本的に「無料の昼食」です。コードを変えなくても、アップグレードするだけで速くなります。
1. Runtime-async がさらに高速化
.NET 11 の大きな方向性の1つは、async/await の実装をコンパイラのステートマシンからランタイム側へ移すことです(runtime-async)。今回も最適化が続いています。
- JIT が同期的に見える
Task返却メソッドに対して、直接専用の非同期版を生成し、余分な間接呼び出しをなくします - 非同期のサスペンドポイントが統合され、生成コードがより小さくなります
- 最も興味深いのは
ExecutionContextの最適化です。以前は各awaitごとに実行コンテキスト(AsyncLocal<T>のような環境状態を保持)を捕捉・復元する必要があり、何も入っていなくてもその処理が走っていました。現在はランタイムが「復元すべきものがない」ことを検出し、その一連の処理をスキップできます。高スループットな非同期コードで特に効果が出ます。
2. JIT の細かな最適化
Math.BigMul は x64 で単一の MUL 命令を生成するようになり、補助関数の呼び出しを経由しなくなりました。両分岐が同じ定数を返す三項演算子は、そのまま定数へ畳み込まれます。ARM64 SVE 環境では Vector<T> が参照渡しに変更されました。これらはコミュニティ開発者の貢献によるものです。
3. NativeAOT のインターフェース呼び出しが高速化
NativeAOT のインターフェースメソッド呼び出しは、ウォームアップ可能なディスパッチ補助器を採用しました。呼び出し箇所が小さくなり、インターフェース呼び出しが多いシーンでスループットも向上します。AOT のサイズと性能を気にする人にとっては朗報です。
4. モバイル向けのプロセス内クラッシュレポート
以前のクラッシュ診断では、プロセス外の監視ツールで収集していたため、「死にかけたプロセス」の内部にしかない情報を取得できないことがありました。今後はクラッシュ時に、プロセス内でまずマネージド呼び出しスタック、モジュール一覧、重要なランタイム状態を固定パスへ書き出すため、モバイルプラットフォームでのクラッシュ調査が容易になります。
まとめ
.NET 11 Preview 6 のキーワードは3つです。
- より安全 — CSRF 防御が既定で有効になり、設定なしで1層分の防御が得られます
- より完全 — 非同期検証が基盤ライブラリから ASP.NET Core まで一貫してつながり、union 型も言語、シリアライズ、Web フレームワークまでエコシステム全体でサポートされます
- より高速 — runtime-async、JIT、NativeAOT が継続的に最適化され、アップグレードするだけで恩恵を受けられます
例年どおり、.NET 11 の正式版は今年11月にリリース予定で、LTS の長期サポート版ではありません。いち早く試したい人は Preview 6 の SDK を入れて触ってみるとよいでしょう。union や拡張インデクサーといった言語機能を使う場合は、プロジェクトファイルに <LangVersion>preview</LangVersion> を追加するのを忘れずに。
(Translated by GPT)
元のリンク:https://mp.weixin.qq.com/s/6s_JaHEAP22-aoIBOFN5zQ?token=308772467&lang=zh_CN&wt.mc_id=MVP_325642