Start Debugging

Миграция с System.Web.HttpContext на Microsoft.AspNetCore.Http.HttpContext

Практическая миграция с System.Web.HttpContext из ASP.NET Framework на HttpContext в ASP.NET Core 11: HttpContext.Current, карта свойств, Server.MapPath, Session и слой совместимости адаптеров System.Web для инкрементальных миграций.

Единственная строка, которая ломает больше миграций ASP.NET Framework, чем любая другая, это HttpContext.Current. В ASP.NET Core её не существует. Нет статического окружающего контекста, к которому можно обратиться из произвольного класса, тип HttpContext это другой тип в другом пространстве имён (Microsoft.AspNetCore.Http.HttpContext, а не System.Web.HttpContext), и большинство свойств, на которые вы полагались, переместились, изменили форму или исчезли. Эта статья сопоставляет старый API с новым для .NET 11 / ASP.NET Core 11, а затем показывает два реальных пути вперёд: чистое переписывание для кода, который вы контролируете, и официальные адаптеры System.Web, когда у вас есть куча общих библиотек, которые передают HttpContext туда-сюда и не могут быть переписаны за один проход.

Для небольшого обработчика или одного контроллера переписывание это час. Для монолита, где HttpContext.Current протянут через слой бизнес-логики в отдельной сборке, закладывайте дни и берите адаптеры, чтобы библиотеки продолжали компилироваться под оба фреймворка, пока вы мигрируете приложение за приложением. Ничего в HTTP-семантике не меняется; меняется то, как вы достаёте запрос, что время жизни теперь строго привязано к запросу, и что нет привязки к потоку, на которую можно опереться.

Почему эта миграция не “найти и заменить”

System.Web.HttpContext и Microsoft.AspNetCore.Http.HttpContext это по-настоящему разные объекты, и различия поведенческие, а не только косметические:

Собственное руководство по миграции HttpContext от Microsoft описывает это как две стратегии, и выбор определяет всё дальнейшее: полное переписывание или адаптеры System.Web для инкрементального перехода.

Что ломается

ОбластьASP.NET FrameworkASP.NET Core 11Серьёзность
Окружающий контекстHttpContext.CurrentIHttpContextAccessor (зарегистрировать через AddHttpContextAccessor)высокая
Время жизни контекстаИногда пригоден после запросаObjectDisposedException после завершения запросавысокая
ПотокобезопасностьЗапрос с привязкой к потокуНет привязки к потоку через awaitвысокая
Запись в ответResponse.Write(s)await Response.WriteAsync(s)средняя
Чтение формы / телаRequest.Form, Request.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низкая
URL запросаRequest.Url, Request.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 это корень проекта (старый ~/), WebRootPath это wwwroot (старый корень статических файлов). Для HTML-кодирования Server.HtmlEncode превращается в System.Net.WebUtility.HtmlEncode или, в DI, во внедрённый HtmlEncoder.

Проверьте: запрос, который загружает файл, разрешается в тот же абсолютный путь, который вы ожидаете, как на Windows, так и на Linux (Path.Combine делает его переносимым).

Шаг 5: Перенесите Session, зная, что она ведёт себя иначе

Session в ASP.NET Core подключаемая, на основе байтов, не сериализуется автоматически и не предлагает блокировки на запрос. Зарегистрируйте её:

// .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. Нет автоматической объектной session, как была у Framework. Руководство по миграции session стоит прочитать, если вы полагались на блокировку session.

Проверьте: установите значение в одном запросе, прочитайте его обратно в следующем; убедитесь, что оно переживает между запросами с тем же cookie session.

Когда переписывание слишком велико: адаптеры 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 пакетом адаптера. Чтобы преобразовывать между двумя представлениями внутри запроса, вы используете кешированные преобразования, что позволяет переписывать точечные места вызова инкрементально:

// .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 не предоставляет их по умолчанию: поток запроса с поиском и полной буферизацией (PreBufferRequestStream) и буферизованный ответ (BufferResponseStream). Если библиотека читает тело дважды или полагается на Response.End(), включите их на соответствующих конечных точках:

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

Верификация

После миграции пройдитесь по этому списку:

Откат

Это миграция кода, а не миграция данных, поэтому откат это git revert ветки. Единственное, за чем нужно следить, это формат состояния session: session ASP.NET Core несовместима на уровне формата с session ASP.NET Framework, поэтому, если вы переключили продакшен-трафик и у пользователей есть живые сессии, откат сбрасывает эти сессии и принуждает к повторному входу. Дайте им завершиться или примите это. Больше ничего здесь не одностороннее.

Подводные камни, которые стоит знать до старта

Если вы делаете это в рамках более широкого перехода между фреймворками, это вписывается в более крупную миграцию с .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.

< Назад