Start Debugging

Von System.Web.HttpContext zu Microsoft.AspNetCore.Http.HttpContext migrieren

Eine praktische Migration vom System.Web.HttpContext des ASP.NET Framework zum HttpContext von ASP.NET Core 11: HttpContext.Current, die Eigenschaften-Zuordnung, Server.MapPath, Session und der Shim der System.Web-Adapter für inkrementelle Migrationen.

Die einzelne Zeile, die mehr ASP.NET-Framework-Migrationen zerstört als jede andere, ist HttpContext.Current. Sie existiert in ASP.NET Core nicht. Es gibt keinen statischen Umgebungskontext, auf den man aus einer beliebigen Klasse zugreifen könnte, der Typ HttpContext ist ein anderer Typ in einem anderen Namespace (Microsoft.AspNetCore.Http.HttpContext, nicht System.Web.HttpContext), und die meisten Eigenschaften, auf die Sie sich verlassen haben, sind verschoben worden, haben ihre Form geändert oder sind verschwunden. Dieser Beitrag ordnet die alte API der neuen für .NET 11 / ASP.NET Core 11 zu und zeigt dann die zwei realen Wege nach vorn: ein sauberes Neuschreiben für Code, den Sie kontrollieren, und die offiziellen System.Web-Adapter, wenn Sie einen Haufen gemeinsam genutzter Bibliotheken haben, die HttpContext herumreichen und nicht in einem Zug neu geschrieben werden können.

Für einen kleinen Handler oder einen einzelnen Controller ist das Neuschreiben eine Stunde. Für einen Monolithen, in dem HttpContext.Current durch eine Geschäftsschicht in einem separaten Assembly gefädelt ist, planen Sie Tage ein und greifen Sie zu den Adaptern, damit die Bibliotheken weiterhin gegen beide Frameworks kompilieren, während Sie Anwendung für Anwendung migrieren. Nichts an der HTTP-Semantik ändert sich; was sich ändert, ist, wie Sie die Anfrage erreichen, dass die Laufzeit jetzt strikt an die Anfrage gebunden ist und dass es keine Thread-Affinität gibt, auf die man sich stützen kann.

Warum diese Migration kein Suchen-und-Ersetzen ist

System.Web.HttpContext und Microsoft.AspNetCore.Http.HttpContext sind wirklich verschiedene Objekte, und die Lücken sind verhaltensbezogen, nicht nur kosmetisch:

Microsofts eigener HttpContext-Migrationsleitfaden rahmt dies als zwei Strategien, und die Wahl steuert alles Folgende: vollständiges Neuschreiben oder System.Web-Adapter für einen inkrementellen Umzug.

Was kaputtgeht

BereichASP.NET FrameworkASP.NET Core 11Schweregrad
UmgebungskontextHttpContext.CurrentIHttpContextAccessor (mit AddHttpContextAccessor registrieren)hoch
Kontext-LebensdauerNach der Anfrage teils nutzbarObjectDisposedException nach Ende der Anfragehoch
Thread-SicherheitAnfrage mit Thread-AffinitätKeine Thread-Affinität über await hinweghoch
In die Antwort schreibenResponse.Write(s)await Response.WriteAsync(s)mittel
Formular / Body lesenRequest.Form, Request.InputStream (sync)await Request.ReadFormAsync(), Request.Body (einmal lesbar)mittel
Antwort-Header / CookiesJederzeit setzbarVor Beginn der Antwort setzen (oder via OnStarting)mittel
Physische PfadeServer.MapPath("~/x")IWebHostEnvironment.ContentRootPath / WebRootPath + Path.Combinemittel
SessionSession["k"], auto-serialisiert, gesperrtHttpContext.Session.GetString/SetString, byte-basiert, kein Lockmittel
HTML-KodierungServer.HtmlEncodeSystem.Net.WebUtility.HtmlEncode / HtmlEncoderniedrig
Anfrage-URLRequest.Url, Request.RawUrlRequest.Scheme/Host/Path/QueryString oder GetDisplayUrl()niedrig

Checkliste vor dem Start

Migrationsschritte

Schritt 1: Registrieren Sie den Accessor und greifen Sie nicht mehr zu HttpContext.Current

Ersetzen Sie den Umgebungszugriff durch explizite Injektion. In 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();

Ein Service, der zuvor HttpContext.Current las, erhält nun IHttpContextAccessor:

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

Speichern Sie accessor.HttpContext nicht in einem Feld. Lesen Sie es jedes Mal am Verwendungspunkt, denn das Feld würde einen Kontext aus einer Anfrage erfassen und ihn an eine andere übergeben, oder an gar keine. Innerhalb eines Controllers oder einer Minimal API haben Sie HttpContext bereits als Eigenschaft oder Parameter, bevorzugen Sie es also, ihn explizit zu übergeben, und überspringen Sie den Accessor ganz.

Prüfen: Die Solution kompiliert ohne System.Web-Referenzen in den neu geschriebenen Projekten, und eine Anfrage, die CurrentUserService ausführt, gibt die erwartete Benutzer-ID zurück.

Schritt 2: Übersetzen Sie die Anfrage-Eigenschaften

Die meisten Request-Member wurden verschoben statt entfernt. Die Zuordnung, die die gängigen Fälle abdeckt:

// .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();

Das Formular oder den Body zu lesen ist asynchron, und der Body ist ein Vorwärts-Stream, den Sie einmal lesen können:

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

Prüfen: Rufen Sie einen Endpunkt auf, der Query, Formular und einen Header liest; stellen Sie sicher, dass die Werte mit dem übereinstimmen, was die Framework-Anwendung für dieselbe Anfrage zurückgab.

Schritt 3: Übersetzen Sie die Antwort und respektieren Sie, wann Header gesetzt werden können

Schreiben ist asynchron, und Header und Cookies müssen gesetzt werden, bevor der Body zu fließen beginnt:

// .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);

Wenn Sie in Middleware sind und Header unmittelbar vor dem Senden der Antwort setzen müssen, verwenden Sie den Callback, statt sie zu spät zu setzen:

// .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);

Prüfen: Inspizieren Sie die Antwort-Header mit curl -i; bestätigen Sie, dass der Header vorhanden ist und Sie unter Last keine response has already started-Ausnahme erhalten.

Schritt 4: Ersetzen Sie Server.MapPath durch IWebHostEnvironment

Server.MapPath("~/App_Data/x.json") hat kein Äquivalent. Injizieren Sie IWebHostEnvironment und kombinieren Sie die Pfade selbst:

// .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 ist das Projektstammverzeichnis (das alte ~/), WebRootPath ist wwwroot (das alte Stammverzeichnis für statische Dateien). Für die HTML-Kodierung wird Server.HtmlEncode zu System.Net.WebUtility.HtmlEncode oder, in DI, einem injizierten HtmlEncoder.

Prüfen: Eine Anfrage, die eine Datei lädt, löst denselben absoluten Pfad auf, den Sie erwarten, sowohl unter Windows als auch unter Linux (das Path.Combine hält es portabel).

Schritt 5: Verschieben Sie Session, im Wissen, dass sie sich anders verhält

Die Session von ASP.NET Core ist opt-in, byte-basiert, wird nicht automatisch serialisiert und bietet kein Locking pro Anfrage. Registrieren Sie sie:

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

Tauschen Sie dann den Indexer gegen die typisierten Helfer:

// .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);

Ein Objekt zu speichern bedeutet, es selbst zu serialisieren (zum Beispiel mit System.Text.Json) und SetString aufzurufen. Es gibt keine automatische Objekt-Session, wie sie das Framework hatte. Der Session-Migrationsleitfaden lohnt sich zu lesen, wenn Sie auf das Session-Locking angewiesen waren.

Prüfen: Setzen Sie einen Wert in einer Anfrage, lesen Sie ihn in der nächsten zurück; bestätigen Sie, dass er über Anfragen hinweg mit demselben Session-Cookie überlebt.

Wenn ein Neuschreiben zu groß ist: die System.Web-Adapter

Wenn HttpContext durch Klassenbibliotheken verwoben ist, die auch eine noch nicht migrierte Framework-Anwendung aufruft, ist es nicht praktikabel, jede Signatur auf einmal neu zu schreiben. Microsoft liefert die System.Web-Adapter genau dafür. Sie implementieren die Form von System.Web.HttpContext über dem ASP.NET-Core-Kontext neu, sodass eine Bibliothek netstandard2.0 anvisieren und beide Runtimes bedienen kann.

Die Pakete, die Sie sehen werden:

In der ASP.NET-Core-Anwendung aktivieren Sie sie:

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

Eine Bibliothek, die System.Web.HttpContext entgegennahm, kompiliert weiter, nachdem Sie die System.Web-Referenz gegen das Adapter-Paket getauscht haben. Um innerhalb einer Anfrage zwischen den beiden Repräsentationen zu konvertieren, verwenden Sie die zwischengespeicherten Konvertierungen, was es Ihnen erlaubt, gezielte Aufrufstellen inkrementell neu zu schreiben:

// .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();

Die Adapter sind nicht kostenlos. Sie fügen gegenüber den nativen APIs Overhead hinzu, nicht jeder Member wird unterstützt, und zwei Verhaltensweisen müssen aktiviert werden, weil ASP.NET Core sie standardmäßig nicht bereitstellt: ein durchsuchbarer, vollständig gepufferter Anfrage-Stream (PreBufferRequestStream) und eine gepufferte Antwort (BufferResponseStream). Wenn eine Bibliothek den Body zweimal liest oder sich auf Response.End() verlässt, aktivieren Sie diese an den relevanten Endpunkten:

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

Verifizierung

Gehen Sie nach der Migration diese Liste durch:

Rollback

Dies ist eine Code-Migration, keine Daten-Migration, also ist der Rollback ein git revert des Branches. Das Einzige, worauf zu achten ist, ist das Format des Session-Zustands: Die Session von ASP.NET Core ist nicht wire-kompatibel mit der Session von ASP.NET Framework, also wenn Sie den Produktionsverkehr umgeleitet haben und Benutzer aktive Sessions haben, verwirft ein Rollback diese Sessions und erzwingt eine erneute Anmeldung. Lassen Sie sie auslaufen oder akzeptieren Sie das. Nichts anderes hier ist eine Einbahnstraße.

Fallstricke, die man vor dem Start kennen sollte

Wenn Sie dies als Teil eines umfassenderen Framework-Umzugs tun, passt dies in die größere Migration von .NET Framework 4.8 zu .NET 11, und Sie werden im selben Schritt wahrscheinlich auch das Hosting-Modell ersetzen, wenn Sie von IWebHostBuilder zu WebApplication.CreateBuilder migrieren. Für die neuen Endpunkte, die während der Migration geschrieben werden, lohnt es sich, die Abwägungen von Minimal APIs gegenüber Controllern abzuwägen, bevor Sie die alte Controller-Form wortwörtlich portieren.

Quellen

Comments

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

< Zurück