Start Debugging

System.Web.HttpContext から Microsoft.AspNetCore.Http.HttpContext へ移行する

ASP.NET Framework の System.Web.HttpContext から ASP.NET Core 11 の HttpContext への実践的な移行: HttpContext.Current、プロパティ対応表、Server.MapPath、Session、そして段階的移行のための System.Web アダプターの互換シム。

ASP.NET Framework の移行を他のどれよりも多く壊す 1 行は HttpContext.Current です。これは ASP.NET Core には存在しません。任意のクラスから手を伸ばせる静的なアンビエントコンテキストはなく、HttpContext 型は別の名前空間にある別の型であり (System.Web.HttpContext ではなく Microsoft.AspNetCore.Http.HttpContext)、依存していたプロパティのほとんどは移動したか、形が変わったか、なくなりました。この記事では .NET 11 / ASP.NET Core 11 における古い API を新しい API に対応づけ、続いて現実的な 2 つの前進の道を示します。自分が管理するコードのためのクリーンな書き直しと、HttpContext を渡し回す共有ライブラリの山があって一度に書き直せない場合の公式 System.Web アダプターです。

小さなハンドラーや 1 つのコントローラーであれば、書き直しは 1 時間です。HttpContext.Current が別のアセンブリにあるビジネス層を貫いて通っているモノリスでは、数日を見込み、アプリケーションごとに移行する間もライブラリが両方のフレームワークに対してコンパイルされ続けるようにアダプターに頼ってください。HTTP のセマンティクスは何も変わりません。変わるのは、リクエストにどう到達するか、ライフタイムが今や厳密にリクエストに紐づくこと、そして頼れるスレッドアフィニティがないことです。

なぜこの移行が検索と置換ではないのか

System.Web.HttpContextMicrosoft.AspNetCore.Http.HttpContext は本当に異なるオブジェクトであり、その差は見た目だけでなく動作上のものです:

Microsoft 自身の HttpContext 移行ガイド はこれを 2 つの戦略として位置づけており、その選択が以下のすべてを左右します。完全な書き直しか、段階的な移動のための System.Web アダプターかです。

何が壊れるか

領域ASP.NET FrameworkASP.NET Core 11深刻度
アンビエントコンテキストHttpContext.CurrentIHttpContextAccessor (AddHttpContextAccessor で登録)
コンテキストのライフタイムリクエスト後も時々使えるリクエスト終了後は ObjectDisposedException
スレッド安全性スレッドアフィニティのあるリクエストawait をまたいでスレッドアフィニティなし
レスポンスへの書き込みResponse.Write(s)await Response.WriteAsync(s)
フォーム / ボディの読み取りRequest.FormRequest.InputStream (sync)await Request.ReadFormAsync()Request.Body (一度だけ読める)
レスポンスヘッダー / cookieいつでも設定可能レスポンス開始前に設定 (または OnStarting 経由)
物理パスServer.MapPath("~/x")IWebHostEnvironment.ContentRootPath / WebRootPath + Path.Combine
SessionSession["k"]、自動シリアライズ、ロックありHttpContext.Session.GetString/SetString、バイトベース、ロックなし
HTML エンコードServer.HtmlEncodeSystem.Net.WebUtility.HtmlEncode / HtmlEncoder
リクエスト URLRequest.UrlRequest.RawUrlRequest.Scheme/Host/Path/QueryString または GetDisplayUrl()

事前チェックリスト

移行手順

手順 1: アクセサーを登録し、HttpContext.Current に手を伸ばすのをやめる

アンビエントアクセスを明示的な注入で置き換えます。Program.cs で:

// .NET 11, ASP.NET Core 11, C# 14
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor(); // enables IHttpContextAccessor

var app = builder.Build();
app.MapControllers();
app.Run();

以前 HttpContext.Current を読んでいたサービスは、今や IHttpContextAccessor を受け取ります:

// .NET 11, ASP.NET Core 11, C# 14
public sealed class CurrentUserService(IHttpContextAccessor accessor)
{
    public string? UserId =>
        accessor.HttpContext?.User.FindFirst("sub")?.Value;
}

accessor.HttpContext をフィールドにキャッシュしないでください。毎回使用する箇所で読み取ってください。なぜなら、フィールドはあるリクエストのコンテキストを捕捉して別のリクエスト、あるいはどのリクエストでもないものに渡してしまうからです。コントローラーや minimal API の中では、すでに HttpContext がプロパティまたはパラメーターとして手に入るので、明示的に渡すことを優先し、アクセサーは完全に省いてください。

検証: ソリューションが書き直したプロジェクトで System.Web への参照なしでコンパイルされ、CurrentUserService を行使するリクエストが期待されるユーザー ID を返すこと。

手順 2: リクエストのプロパティを変換する

ほとんどの Request メンバーは消えたのではなく移動しました。一般的なケースをカバーする対応:

// .NET 11, ASP.NET Core 11, C# 14
string method      = httpContext.Request.Method;          // was HttpMethod
bool   isHttps     = httpContext.Request.IsHttps;         // was IsSecureConnection
string? remoteIp   = httpContext.Connection.RemoteIpAddress?.ToString(); // was UserHostAddress
string userAgent   = httpContext.Request.Headers.UserAgent.ToString();

// Query string: IQueryCollection, indexer never throws on a missing key
string q = httpContext.Request.Query["key"].ToString(); // "" if absent

// Full URL: no single Request.Url anymore
// using Microsoft.AspNetCore.Http.Extensions;
string url = httpContext.Request.GetDisplayUrl();

フォームやボディの読み取りは非同期であり、ボディは一度だけ読める前方専用のストリームです:

// .NET 11, ASP.NET Core 11, C# 14
if (httpContext.Request.HasFormContentType)
{
    IFormCollection form = await httpContext.Request.ReadFormAsync();
    string firstName = form["firstname"].ToString();
}

検証: query、フォーム、ヘッダーを読むエンドポイントを叩き、値が同じリクエストに対して Framework アプリケーションが返していたものと一致することを確認します。

手順 3: レスポンスを変換し、ヘッダーをいつ設定できるかを尊重する

書き込みは非同期であり、ヘッダーと cookie はボディが流れ始める前に設定しなければなりません:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Response.StatusCode = StatusCodes.Status200OK;
httpContext.Response.ContentType = "application/json";
httpContext.Response.Headers["X-Custom"] = "value"; // before first write
await httpContext.Response.WriteAsync(payload);

middleware の中にいて、レスポンスが送信される直前にヘッダーを設定する必要がある場合は、遅れて設定するのではなくコールバックを使います:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Response.OnStarting(static state =>
{
    var ctx = (HttpContext)state;
    ctx.Response.Headers["X-Late"] = "value";
    return Task.CompletedTask;
}, httpContext);

検証: curl -i でレスポンスヘッダーを確認し、ヘッダーが存在し、負荷下で response has already started 例外が出ないことを確かめます。

手順 4: Server.MapPath を IWebHostEnvironment で置き換える

Server.MapPath("~/App_Data/x.json") には同等品がありません。IWebHostEnvironment を注入し、パスは自分で組み立てます:

// .NET 11, ASP.NET Core 11, C# 14
public sealed class FileService(IWebHostEnvironment env)
{
    public string DataPath(string name) =>
        Path.Combine(env.ContentRootPath, "App_Data", name); // project root
    public string AssetPath(string name) =>
        Path.Combine(env.WebRootPath, name);                 // wwwroot
}

ContentRootPath はプロジェクトルート (旧 ~/)、WebRootPathwwwroot (旧静的ファイルルート) です。HTML エンコードについては、Server.HtmlEncodeSystem.Net.WebUtility.HtmlEncode か、DI では注入された HtmlEncoder になります。

検証: ファイルを読み込むリクエストが、Windows でも Linux でも期待どおりの同じ絶対パスに解決されること (Path.Combine がポータブルに保ちます)。

手順 5: Session を移すが、振る舞いが異なることを知っておく

ASP.NET Core の session はオプトインで、バイトベースであり、自動シリアライズされず、リクエストごとのロックも提供しません。登録します:

// .NET 11, ASP.NET Core 11, C# 14
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
// ...
app.UseSession(); // before endpoints

次にインデクサーを型付きヘルパーに置き換えます:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Session.SetString("user", "marius"); // was Session["user"] = "marius"
string? user = httpContext.Session.GetString("user");
httpContext.Session.SetInt32("count", 3);

オブジェクトを保存するということは、自分でシリアライズして (例えば System.Text.Json で) SetString を呼ぶことを意味します。Framework が持っていたような自動のオブジェクト session はありません。session のロックに依存していたなら、session 移行ガイド を読む価値があります。

検証: あるリクエストで値を設定し、次のリクエストで読み戻します。同じ session cookie で複数のリクエストをまたいで生き残ることを確認します。

書き直しが大きすぎるとき: System.Web アダプター

HttpContext が、まだ移行していない Framework アプリケーションも呼び出すクラスライブラリ群に織り込まれている場合、すべてのシグネチャを一度に書き直すのは現実的ではありません。Microsoft はまさにこのために System.Web アダプター を提供しています。これらは System.Web.HttpContext の形を ASP.NET Core のコンテキストの上に再実装するので、ライブラリは netstandard2.0 をターゲットにして両方のランタイムを提供できます。

目にするパッケージ:

ASP.NET Core アプリケーションでオプトインします:

// .NET 11, ASP.NET Core 11, C# 14
builder.Services.AddSystemWebAdapters();
// ...
app.UseSystemWebAdapters();

System.Web.HttpContext を受け取っていたライブラリは、System.Web への参照をアダプターパッケージに置き換えた後もコンパイルされ続けます。リクエスト内で 2 つの表現を変換するには、キャッシュされた変換を使います。これにより、対象を絞った呼び出し箇所を段階的に書き直せます:

// .NET 11, ASP.NET Core 11, C# 14
// Microsoft.AspNetCore.Http.HttpContext -> System.Web.HttpContext
System.Web.HttpContext legacy = coreContext.AsSystemWeb();
// System.Web.HttpContext -> Microsoft.AspNetCore.Http.HttpContext
HttpContext core = legacy.AsAspNetCore();

アダプターは無料ではありません。ネイティブ API に比べてオーバーヘッドを追加し、すべてのメンバーがサポートされているわけではなく、ASP.NET Core がデフォルトで提供しないため 2 つの振る舞いはオプトインが必要です。シーク可能で完全にバッファリングされたリクエストストリーム (PreBufferRequestStream) と、バッファリングされたレスポンス (BufferResponseStream) です。ライブラリがボディを 2 回読むか Response.End() に依存している場合は、該当するエンドポイントでこれらを有効にします:

// .NET 11, ASP.NET Core 11, C# 14
app.MapDefaultControllerRoute()
   .PreBufferRequestStream()
   .BufferResponseStream();

検証

移行後、このリストを一通り確認します:

ロールバック

これはコードの移行であり、データの移行ではないので、ロールバックはブランチの git revert です。注意すべき唯一の点は session 状態の形式です。ASP.NET Core の session は ASP.NET Framework の session とワイヤー互換ではないため、本番トラフィックを切り替えてユーザーがアクティブな session を持っている場合、ロールバックはそれらの session を破棄し、再ログインを強制します。それらを排出するか、受け入れてください。ここの他のものは一方通行ではありません。

始める前に知っておく価値のある落とし穴

これをより広いフレームワーク移行の一部として行う場合、これはより大きな .NET Framework 4.8 から .NET 11 への移行 の中に収まり、IWebHostBuilder から WebApplication.CreateBuilder への移行 を行う同じ工程で、ホスティングモデルもおそらく置き換えることになります。移行中に書く新しいエンドポイントについては、古いコントローラーの形をそのまま移植する前に、minimal API 対コントローラー のトレードオフを比較検討する価値があります。

出典

Comments

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

< 戻る