←記事一覧

ASP.NET Core の内部構造 — リクエストが到達してからレスポンスを返すまで

ASP.NET Core がリクエストを受けてからレスポンスを返すまでの処理を、ホスティングモデル、ミドルウェアパイプライン、DI コンテナー、ルーティング、フィルターパイプラインなどの内部実装に沿って、簡略化したコードと図で説明します。

ASP.NET Core.NETArchitectureMiddlewareDependency Injection

はじめに

ASP.NET Core は、Microsoft が開発するクロスプラットフォームの Web フレームワークです。app.MapGet("/hello", () => "Hello World") の一行で書いたハンドラーにも、リクエストは Kestrel での HTTP 解析、DI スコープの作成、ミドルウェアパイプライン、ルーティングなどを経て届きます。

以降の章では、この経路の各段階を担うコンポーネントと、構成・エラーハンドリング・テストなどの周辺の仕組みについて、内部実装を簡略化したコードと図で説明します。

全体アーキテクチャの俯瞰

リクエストは Kestrel から HostingApplication を経てミドルウェアパイプラインに入り、登録順にミドルウェアを通ってエンドポイントのハンドラーに届きます。レスポンスは同じミドルウェアを逆順にたどって返ります。

第1章:ホスティングモデル — アプリケーションが起動するまで

Generic Host の構造

ASP.NET Core アプリケーションは Generic Host(Microsoft.Extensions.Hosting)の上に構築されます。WebApplication.CreateBuilder() を呼び出すと、内部では次の処理が行われます。

var builder = WebApplication.CreateBuilder(args);
// 内部で起こること:
// 1. HostApplicationBuilder を初期化
// 2. Kestrel を既定の Web サーバーとして設定
// 3. 設定ソース (appsettings.json, 環境変数等) をロード
// 4. ログプロバイダーを設定
// 5. DI コンテナ (ServiceCollection) を初期化

WebApplication.CreateBuilder() 自体は、引数を WebApplicationOptions に渡して WebApplicationBuilder を生成するだけです。

// dotnet/aspnetcore の簡略化した実装イメージ
public static WebApplicationBuilder CreateBuilder(string[] args)
{
    var builder = new WebApplicationBuilder(
        new WebApplicationOptions { Args = args });
    return builder;
}

構成ソースの登録や ServiceCollection の作成は WebApplicationBuilder のコンストラクターで進み、Build() で DI コンテナーと WebApplication が作られます。app.UseXxx() で登録したミドルウェアは、app.Run() でホストが起動するときにパイプラインとして組み立てられます。

Kestrel — 既定の HTTP サーバー

Kestrel は ASP.NET Core の既定のインプロセス HTTP サーバーです。当初の libuv ベースの実装から独自の I/O 実装に移行し、現在は System.IO.Pipelines と System.Net.Sockets を使った非同期 I/O で動作しています。

Kestrel の内部構造

Kestrel は受信データを System.IO.Pipelines で読み取ります。従来の Stream ベース API では、データを読み取る際にバッファーのコピーが頻繁に発生していました。Pipelines では、受信バッファーを ReadOnlySequence<byte> として参照するので、コピーをほとんど伴わずにデータを解析できます。

// Kestrel の HTTP/1.1 パーサーの簡略イメージ
while (true)
{
    // PipeReader からデータを読み取り(コピー不要)
    ReadResult result = await reader.ReadAsync();
    ReadOnlySequence<byte> buffer = result.Buffer;
 
    // HTTP リクエストラインとヘッダーをパース
    if (TryParseRequest(buffer, out var consumed, out var examined))
    {
        reader.AdvanceTo(consumed, examined);
        break;
    }
 
    // データが不足している場合、追加データを待つ
    reader.AdvanceTo(buffer.Start, buffer.End);
}

接続管理と同時接続

Kestrel は各接続を個別の Task として管理します。.NET のスレッドプールと async/await により、I/O を待つ間はスレッドを解放するので、少数の OS スレッドで大量の同時接続を処理できます。

接続数・リクエストサイズ・タイムアウトの上限は KestrelServerLimits で設定でき、既定値は次のとおりです(Microsoft Learn)。

パラメーター既定値説明
MaxConcurrentConnectionsnull(無制限)同時接続数の上限
MaxConcurrentUpgradedConnectionsnull(無制限)WebSocket 等のアップグレード接続上限
MaxRequestBodySize約30 MB(30,000,000バイト)リクエストボディーの最大サイズ
RequestHeadersTimeout30秒リクエストヘッダー受信のタイムアウト
KeepAliveTimeout130秒Keep-Alive 接続のタイムアウト

リバースプロキシー構成

本番環境では、Kestrel をリバースプロキシー(Nginx, IIS, YARP 等)の背後に配置するのが一般的です。

リバースプロキシーには、主に次の処理を任せられます。

  • TLS 終端:SSL/TLS の暗号化と復号を担う
  • ロードバランシング:複数の Kestrel インスタンスに負荷を分散する
  • リクエストバッファリング:遅いクライアントからの送信をプロキシー側で受け切る
  • 静的ファイル配信:Kestrel に到達させずにレスポンスを返す
  • レート制限:DDoS 攻撃や過剰なリクエストを緩和する

第2章:ミドルウェアパイプライン — リクエストの旅

ミドルウェアの仕組み

ASP.NET Core では、リクエストはミドルウェアパイプラインを通って処理されます。パイプラインの各段は、次の段を表す RequestDelegate を受け取り、自身の処理を加えた RequestDelegate を返します。この組み立て方により、ミドルウェアは入れ子になります。各ミドルウェアは、_next を呼ぶ前にリクエストを、呼んだ後にレスポンスを処理できます。

// RequestDelegate の定義
public delegate Task RequestDelegate(HttpContext context);
 
// ミドルウェアの基本形
public class MyMiddleware
{
    private readonly RequestDelegate _next;
 
    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        // リクエスト処理(パイプラインの下流へ行く前)
        Console.WriteLine("Before next middleware");
 
        await _next(context); // 次のミドルウェアを呼び出す
 
        // レスポンス処理(パイプラインの下流から戻ってきた後)
        Console.WriteLine("After next middleware");
    }
}

パイプラインの構築プロセス

app.Run() でホストが起動するとき、app.UseXxx() で登録されたミドルウェアが逆順にチェーンされます。最後に登録したミドルウェアから順に包んでいくので、最初に登録したミドルウェアが一番外側になり、リクエストを最初に受け取ります。この入れ子の構造は、ロシアンドール(マトリョーシカ人形)モデルと呼ばれます。

ApplicationBuilder.Build() は、404を返す終端のデリゲートから始めて、登録されたミドルウェアを後ろから順に包みます。

// ApplicationBuilder.Build() の簡略化した実装
public RequestDelegate Build()
{
    // 最終段:どのミドルウェアも処理しなかった場合 404 を返す
    RequestDelegate app = context =>
    {
        context.Response.StatusCode = 404;
        return Task.CompletedTask;
    };
 
    // 登録されたミドルウェアを逆順に適用
    for (int i = _components.Count - 1; i >= 0; i--)
    {
        app = _components[i](app);
    }
 
    return app;
}

短絡(Short-Circuit)

ミドルウェアは next() を呼ばないことで、パイプラインを短絡できます。例えば StaticFiles ミドルウェアは、該当するファイルが見つかると、後続のミドルウェアを実行せずにレスポンスを返します。

組み込みミドルウェアの順序

ミドルウェアは登録順に実行されるので、順序が動作を左右します。例えば Authorization を Authentication より前に置くと、認可の時点ではまだユーザーが認証されていません。推奨される順序は次のとおりです(Microsoft Learn)。

順序ミドルウェア責務
1ExceptionHandler / DeveloperExceptionPage例外をキャッチしてエラーレスポンスに変換
2HSTSHTTP Strict Transport Security ヘッダー追加
3HttpsRedirectionHTTP → HTTPS リダイレクト
4StaticFiles / MapStaticAssets静的ファイルの配信(短絡可能)。.NET 9以降は MapStaticAssets が推奨(ビルド時圧縮・フィンガープリント対応)
5RoutingURL に基づいてエンドポイントを選択
6RateLimiterレート制限ポリシーの適用
7CorsCross-Origin Resource Sharing ポリシー適用
8Authenticationユーザー認証(Identity 確立)
9Authorizationユーザー認可(アクセス制御)
10OutputCacheレスポンスをサーバー側でキャッシュ
11エンドポイントミドルウェアMapGet() / MapControllers() 等

第3章:依存性注入(DI)コンテナーの内部実装

ASP.NET Core の DI コンテナー

ASP.NET Core は、Microsoft.Extensions.DependencyInjection パッケージとして提供される DI コンテナーを組み込みで使います。アプリケーションのサービスだけでなく、フレームワーク自身のサービスもこのコンテナーに登録されます。

サービスのライフタイム

DI コンテナーは3種類のライフタイムをサポートします。

ライフタイム生成タイミング破棄タイミング
Transient注入のたびに新規作成スコープの終了時
Scopedスコープ内で初回要求時スコープの終了時(リクエスト終了時)
Singleton初回要求時アプリケーション終了時

Captive Dependency(キャプティブ依存関係)問題

Singleton サービスが Scoped サービスに依存すると、Singleton が保持する参照を通じて、Scoped サービスはリクエストをまたいで使い回されます。Scoped サービスが事実上 Singleton として動作するこの状態を、キャプティブ依存関係と呼びます。

// 危険:Singleton が Scoped に依存
services.AddSingleton<MySingleton>();
services.AddScoped<MyScopedService>();
 
public class MySingleton
{
    private readonly MyScopedService _scoped; // この参照がリーク
 
    public MySingleton(MyScopedService scoped)
    {
        _scoped = scoped; // リクエストをまたいで保持されてしまう
    }
}

ASP.NET Core には、この依存関係を検出して例外をスローする検証機能があります。WebApplication.CreateBuilder() を使う場合、開発環境では ValidateScopes と ValidateOnBuild が既定で有効です(.NET 6以降)。本番環境でも有効にしたい場合や、WebApplicationBuilder 以外の方法でホストを構築する場合は、次のように設定します。

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;  // 開発環境では既定で有効
    options.ValidateOnBuild = true; // ビルド時に依存関係を検証
});

ServiceProvider の内部実装

ServiceProvider は、要求された型を作るのに必要な依存関係を CallSite のグラフにまとめ、ライフタイムごとのキャッシュを使ってインスタンスを返します。

あるサービスを初めて解決するとき、CallSiteFactory が CallSite のグラフを構築します。最初はこのグラフをたどってインスタンスを生成し、同じサービスが繰り返し解決されると、グラフからコンパイルしたファクトリーデリゲートに切り替えて高速に解決します。

// ServiceProvider の解決プロセスの概要
// 1. CallSiteFactory が ServiceDescriptor から CallSite ツリーを構築
// 2. RuntimeResolver / CompiledResolver が CallSite を走査してインスタンスを生成
// 3. ライフタイムに応じたキャッシュに格納
 
// 高頻度で呼ばれるため、以下の最適化が適用されています(.NET Core 3.0 以降改善が継続):
// - IL Emit によるファクトリの動的コンパイル
// - Expression Tree から Delegate への変換
// - キャッシュ済み Singleton の直接返却

スコープの仕組み

HTTP リクエストごとに IServiceScopeFactory で新しいスコープが作成され、レスポンスの完了後に破棄されます。スコープの破棄では、そのスコープで生成された IDisposable な Scoped / Transient サービスも破棄されます。

第4章:ルーティングシステム

エンドポイントルーティング

ASP.NET Core は .NET Core 3.0以降、エンドポイントルーティングを採用しており、ルーティングの処理は2つのミドルウェアに分かれています。

UseRouting() がエンドポイントを選んでから、UseEndpoints() / MapXxx() 側で実行するまでの間に認証・認可ミドルウェアが動きます。そのため、認証・認可ミドルウェアは選択されたエンドポイントのメタデータ([Authorize] 属性など)を参照して動作できます。

ルートマッチングの内部

ルーティングエンジンは DFA(Deterministic Finite Automaton:決定性有限オートマトン)ベースのマッチャーを使います。DFA マッチャーは、URL のセグメントを1つずつ読んで状態を遷移させます。

DFA マッチャーには次の特徴があります。

  • マッチングの時間計算量は O(パスのセグメント数) で、全ルートを線形に走査しない
  • オートマトンは、マッチングの前にルートテーブルから構築しておく
  • ルートパラメーターの制約({id:int})もマッチングの段階で評価する
// ルートパターンの例と内部的なマッチング
app.MapGet("/api/users/{id:int}", (int id) => ...);
// パターン: Literal("api") -> Literal("users") -> Parameter("id", IntConstraint)
 
app.MapGet("/api/users/{id:guid}", (Guid id) => ...);
// パターン: Literal("api") -> Literal("users") -> Parameter("id", GuidConstraint)
 
// 同じパスでも制約が異なれば別のエンドポイントにマッチ
// /api/users/42       → int 版
// /api/users/3f2504e0-4f89-11d3-9a0c-0305e82c3301  → guid 版
// /api/users/abc-def  → どちらにも一致しない(404)

Minimal API と Controller の比較

ASP.NET Core には、Minimal API と Controller(MVC)の2つのプログラミングモデルがあります。Minimal API では、ハンドラーごとに RequestDelegateFactory がデリゲートを生成します。Controller では、起動時にフレームワークがコントローラーを解析し、アクションをフィルターパイプライン経由で実行します。

特徴Minimal APIController (MVC)
ルーティングapp.MapGet/Post/Put/Delete()属性ルーティング [Route] / 規約ベース
モデルバインディングRequestDelegateFactory が自動推論ModelBinder + ValueProvider チェーン
フィルターEndpoint Filter(ASP.NET Core 7+)Action / Result / Exception / Resource
適用場面シンプルな API、マイクロサービス複雑なアプリ、View を使う MVC
起動速度高速(リフレクション最小)やや遅い(Controller のスキャン・ディスクリプター構築)

第5章:モデルバインディングとバリデーション

モデルバインディングの流れ

モデルバインディングは、HTTP リクエストの生データ(クエリー文字列、ルートパラメーター、フォーム、JSON ボディー等)を C# のオブジェクトに変換する処理です。MVC では、取得した値の検証までがこの流れに含まれます。

バインディングソースの種類

属性を指定しない場合、値はフォームフィールド → リクエストボディー([ApiController] が付いたコントローラー)→ ルートデータ → クエリー文字列 → アップロードファイルの順に探されます。ルートデータとクエリー文字列は主に単純型、アップロードファイルは IFormFile / IEnumerable<IFormFile> にだけ使われます。属性を明示すると、次のソースからデータを取得できます。

  • フォームデータ([FromForm])
  • リクエストボディー([FromBody]):入力フォーマッター経由で処理
  • ルートパラメーター([FromRoute])
  • クエリー文字列([FromQuery])
  • ヘッダー([FromHeader])
  • DI コンテナーのサービス([FromServices])
// バインディングソースの明示指定
[HttpPost("api/orders/{id}")]
public IActionResult UpdateOrder(
    [FromRoute] int id,           // URL: /api/orders/42
    [FromQuery] bool notify,      // Query: ?notify=true
    [FromBody] OrderDto order,    // Body: JSON
    [FromHeader(Name = "X-Correlation-Id")] string correlationId,
    [FromServices] IOrderService service)  // DI コンテナ
{
    // ...
}

JSON シリアライズ:System.Text.Json

ASP.NET Core は既定で System.Text.Json を使います。Newtonsoft.Json から移行すると、既定の解析が厳格になる点と、ポリモーフィズムの指定方法が変わる点が既存コードの動作に影響します。主な違いは次のとおりです。

特徴System.Text.JsonNewtonsoft.Json
パフォーマンスUtf8JsonReader/Writer による高速処理リフレクション重視
メモリー効率Span<byte> ベース、アロケーション最小string ベース
Source GeneratorJsonSerializerContext で AOT 対応なし
既定の動作厳格(コメント不可、末尾のコンマ不可)寛容
ポリモーフィズム[JsonDerivedType](.NET 7+)TypeNameHandling(セキュリティーリスクあり)
// Source Generator を使った高速 JSON シリアライズ
[JsonSerializable(typeof(WeatherForecast))]
[JsonSerializable(typeof(List<WeatherForecast>))]
public partial class AppJsonContext : JsonSerializerContext { }
 
// Program.cs で登録
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain
        .Insert(0, AppJsonContext.Default);
});

第6章:フィルターパイプライン(MVC)

フィルターの実行順序

MVC のフィルターは、アクションの実行前後に処理を差し込む仕組みです。認可・リソース・アクション・例外・結果の5種類があり、次の順序で実行されます。

Endpoint Filter(Minimal API 用)

Minimal API では、MVC フィルターの代わりに Endpoint Filter を使います(ASP.NET Core 7以降)。次の例では、ハンドラーを呼ぶ前に引数 id を検査し、正の整数でなければ400 Bad Request を返します。

app.MapGet("/api/users/{id}", (int id) => ...)
    .AddEndpointFilter(async (context, next) =>
    {
        var id = context.GetArgument<int>(0);
        if (id <= 0)
        {
            return Results.BadRequest("ID は正の整数である必要があります");
        }
 
        // 次のフィルタまたはハンドラを実行
        return await next(context);
    });

第7章:構成(Configuration)システム

構成プロバイダーのチェーン

ASP.NET Core の構成システムは複数のソースから設定値を読み込み、同じキーがあれば、後から追加されたソースの値で前のソースの値を上書きします。

// 構成値の取得方法
// 1. IConfiguration から直接取得
var connectionString = builder.Configuration
    .GetConnectionString("DefaultConnection");
 
// 2. Options パターン(推奨)
builder.Services.Configure<SmtpSettings>(
    builder.Configuration.GetSection("Smtp"));
 
// 3. 型安全な Options with validation
builder.Services.AddOptions<SmtpSettings>()
    .BindConfiguration("Smtp")
    .ValidateDataAnnotations()
    .ValidateOnStart(); // 起動時にバリデーション実行

Options パターンの3つのインターフェース

Options パターンでは、構成の再読み込みが必要か、どのライフタイムのサービスに注入するかに応じて、3つのインターフェースを使い分けます。

インターフェース用途リロード対応
IOptions<T>Singleton。初回アクセス時に値を作成してキャッシュ不可
IOptionsSnapshot<T>Scoped。スコープ(Web では通常リクエスト)ごとに値を再計算可。ただし Singleton には注入しない
IOptionsMonitor<T>Singleton。現在値と変更通知を管理可(OnChange コールバック)
// IOptionsMonitor で構成変更をリアルタイム監視
public class MyService
{
    private readonly IOptionsMonitor<MyConfig> _config;
 
    public MyService(IOptionsMonitor<MyConfig> config)
    {
        _config = config;
        _config.OnChange(newConfig =>
        {
            Console.WriteLine($"構成が変更されました: {newConfig.SomeKey}");
        });
    }
 
    public string GetValue() => _config.CurrentValue.SomeKey;
}

第8章:認証と認可

認証スキームとハンドラー

ASP.NET Core の認証は、スキームごとに登録したハンドラー(JWT Bearer、Cookie など)が行い、成功すると ClaimsPrincipal が作られます。認可ミドルウェアは、この ClaimsPrincipal を使ってポリシーを評価します。

認証の処理フロー

// AuthenticationMiddleware の簡略化した実装
public async Task InvokeAsync(HttpContext context)
{
    // 1. 既定のスキームで認証を試行
    var result = await context.AuthenticateAsync();
 
    if (result.Succeeded)
    {
        // 2. ClaimsPrincipal を HttpContext に設定
        context.User = result.Principal;
    }
 
    // 3. 次のミドルウェア(通常は Authorization)へ
    await _next(context);
}

ポリシーベース認可

認可の条件は、名前付きのポリシーとして登録します。ロールの確認のような組み込みの条件に加え、要件(Requirement)とそれを判定するハンドラーを実装すれば、独自の条件も表せます。次の例の MinimumAgeHandler は、birth_date クレームから年齢を計算し、18歳以上なら要件を満たしたと判定します。

// カスタム認可ポリシーの定義
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));
 
    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
 
// カスタム AuthorizationHandler
public class MinimumAgeHandler
    : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var birthDateClaim = context.User.FindFirst("birth_date");
        if (birthDateClaim is null)
        {
            return Task.CompletedTask; // Fail(明示的に Succeed しない)
        }
 
        var birthDate = DateOnly.Parse(birthDateClaim.Value);
        var age = DateOnly.FromDateTime(DateTime.Today)
            .DayNumber - birthDate.DayNumber;
 
        if (age / 365 >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }
 
        return Task.CompletedTask;
    }
}

第9章:HttpContext — リクエストの全情報

HttpContext の構造

HttpContext は1つの HTTP リクエスト/レスポンスサイクルに関するすべての情報を保持するオブジェクトです。

Feature コレクション

HttpContext.Features は、インターフェースの型をキーにした機能のコレクションです。サーバーやミドルウェアはここに機能を追加し、アプリケーションはその型で取り出して使います。接続情報はサーバー(Kestrel)が設定し、リクエストのタイムアウトは RequestTimeouts ミドルウェアが追加します。

// Kestrel が提供する Feature の例
var connectionFeature = context.Features
    .Get<IHttpConnectionFeature>();
var localIp = connectionFeature?.LocalIpAddress;
 
// HTTP レスポンストレーラー(レスポンスボディの後に送信される末尾ヘッダー)
var trailersFeature = context.Features
    .Get<IHttpResponseTrailersFeature>();
 
// リクエストのタイムアウト (.NET 8+)
var timeoutFeature = context.Features
    .Get<IHttpRequestTimeoutFeature>();
timeoutFeature?.DisableTimeout();

第10章:パフォーマンス最適化のアーキテクチャ

Object Pool

ASP.NET Core は頻繁に使うオブジェクトをプールして再利用し、GC の負荷を減らしています。アプリケーションのコードでも ObjectPool<T> を使えば同じことができます。

// StringBuilder のプーリング例
var pool = serviceProvider
    .GetRequiredService<ObjectPool<StringBuilder>>();
 
var sb = pool.Get();
try
{
    sb.Append("Hello ");
    sb.Append("World");
    return sb.ToString();
}
finally
{
    pool.Return(sb); // プールに返却(Clear も自動)
}

ArrayPool と MemoryPool

System.Buffers.ArrayPool<T> は配列のレンタル・返却を管理し、大量の短命な配列によるヒープ割り当てを削減します。Kestrel 自身は ArrayPool を使わず、固定(pinned)されたブロックを持つ MemoryPool<byte> からソケット I/O 用のバッファーを借り、System.IO.Pipelines に渡します。アプリケーションのコードでは、同じ借りて返すパターンを ArrayPool で書けます。

// アプリケーションコードでの借用・返却パターン
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
    int bytesRead = await stream.ReadAsync(
        buffer.AsMemory(0, 4096));
    ProcessData(buffer.AsSpan(0, bytesRead));
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

Response Compression

レスポンス圧縮ミドルウェアは、Accept-Encoding ヘッダーに基づいて自動的にレスポンスを圧縮します。HTTPS での圧縮は、CRIME 攻撃や BREACH 攻撃のリスクがあるため既定で無効です(Microsoft Learn)。次の例では EnableForHttps で有効にしています。

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true; // HTTPS でも圧縮
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});
 
builder.Services.Configure<BrotliCompressionProviderOptions>(
    options => options.Level = CompressionLevel.Fastest);

.NET 8以降のパフォーマンス改善と新機能

各表の ASP.NET Core の機能は、Microsoft Learn の What's new in ASP.NET Core(.NET 8、.NET 9、.NET 10)に載っています。.NET 8の表の Frozen Collections、SearchValues、CompositeFormat は .NET 8のライブラリーに追加された機能で、What's new in .NET 8 runtime に載っています。

.NET 8

機能説明
Native AOTASP.NET Core が対応(gRPC、Minimal API、Worker Service。コンソールアプリは .NET 7から対応)。JIT 不要。起動時間とメモリー消費を削減
Request Delegate GeneratorMinimal API のデリゲートをコンパイル時に生成
Frozen CollectionsFrozenDictionary / FrozenSet で読み取り専用コレクションを最適化
SearchValuesスパン内で特定の文字やバイトの集合を探す検索を最適化(部分文字列の検索は .NET 9で追加)
Interceptorsコンパイル時にメソッド呼び出しを差し替え(C# 12ではプレビュー機能)
CompositeFormatstring.Format のパース済みフォーマット文字列

.NET 9

機能説明
MapStaticAssetsビルド時圧縮・フィンガープリント付き静的ファイル配信(UseStaticFiles の推奨代替)
HybridCacheIDistributedCache と IMemoryCache を統合する新キャッシュライブラリー。スタンピード防止機能付き(.NET 9のリリース時点ではプレビュー。最初の安定版は2025年3月の 9.3.0)
Keyed DI in Middlewareミドルウェアのコンストラクターや Invoke メソッドで [FromKeyedServices] をサポート
SignalR Native AOTSignalR のクライアント/サーバーがトリミングと Native AOT に対応

.NET 10

機能説明
Minimal API ValidationAddValidation() で DataAnnotations ベースの組み込みバリデーションを有効化
Server-Sent EventsTypedResults.ServerSentEvents で SSE ストリームを返却可能
OpenAPI 3.1JSON Schema draft 2020-12に準拠した OpenAPI ドキュメント生成
Memory Pool EvictionKestrel / IIS / HTTP.sys のメモリープールがアイドル時に自動解放
JSON Patch (System.Text.Json)Newtonsoft.Json 不要の JSON Patch 実装

第11章:エラーハンドリングのアーキテクチャ

例外処理の階層

MVC のアクション内で発生した例外は、まず例外フィルターが処理します。フィルターで処理されなかった例外は、パイプラインの上流にある ExceptionHandler(開発環境では DeveloperExceptionPage)ミドルウェアに届きます。そこでも処理されなければ500が返ります。

Problem Details(RFC 9457)

ASP.NET Core 7では、Problem Details サービス(IProblemDetailsService)が追加されました(リリースノート)。AddProblemDetails() でこのサービスを登録すると、UseExceptionHandler() と UseStatusCodePages() で追加したミドルウェアが、エラーレスポンスを Problem Details 形式で返します(Microsoft Learn)。この形式は RFC 7807で定義され、2023年に RFC 9457 へ置き換えられました。

// グローバルに Problem Details を有効化
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] =
            context.HttpContext.TraceIdentifier;
    };
});
 
// 例外ハンドラーでの使用
app.UseExceptionHandler();
app.UseStatusCodePages();
 
// レスポンス例:
// {
//   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
//   "title": "Not Found",
//   "status": 404,
//   "traceId": "00-abc123-def456-01"
// }

第12章:テストアーキテクチャ

WebApplicationFactory

WebApplicationFactory は、アプリケーション全体をインメモリーの TestServer 上で起動します。テストの HttpClient からのリクエストはネットワークを通らずに、本番と同じミドルウェアパイプラインで処理されます。DI に登録したサービスは、テスト用の実装に差し替えることができます。

public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
 
    public ApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // テスト用にサービスを差し替え
                services.AddScoped<IEmailService, FakeEmailService>();
            });
        }).CreateClient();
    }
 
    [Fact]
    public async Task GetUsers_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/users");
        response.EnsureSuccessStatusCode();
 
        var users = await response.Content
            .ReadFromJsonAsync<List<UserDto>>();
        Assert.NotEmpty(users);
    }
}

まとめ:リクエストの生涯

1つの HTTP リクエストは、Kestrel での解析、スコープの作成、ミドルウェア、ルーティング、認証・認可、モデルバインディング、フィルターを通ってアクションに届き、同じ経路を戻ってレスポンスになります。

参考資料