Перенос HttpContext из ASP.NET Framework в ASP.NET Core

HttpContext — это фундаментальный компонент веб-приложений, предоставляющий доступ к данным HTTP-запроса и ответа. При переходе с ASP.NET Framework на ASP.NET Core HttpContext представляет уникальные проблемы, так как две платформы имеют разные API и подходы.

Почему миграция HttpContext является сложной

ASP.NET Framework и ASP.NET Core имеют принципиально разные реализации HttpContext:

Эти различия означают, что вы не можете просто переместить код HttpContext из Платформы в Core без изменений.

Обзор стратегий миграции

Во время миграции существует два основных подхода к обработке HttpContext:

  1. Полная перезапись - перепишите весь код HttpContext для использования собственной реализации HttpContext в ASP.NET Core.
  2. Адаптеры System.Web — используйте адаптеры для минимизации изменений кода при переносе постепенно

Для большинства приложений миграция на собственный HttpContext ядра ASP.NET обеспечивает оптимальную производительность и удобство обслуживания. Однако более крупные приложения или те, у которых широкое использование HttpContext, могут воспользоваться адаптерами System.Web во время добавочной миграции.

Выбор способа миграции

У вас есть два основных варианта переноса HttpContext из ASP.NET Framework в ASP.NET Core. Выбор зависит от временной шкалы миграции, нужно ли одновременно запускать оба приложения и сколько кода вы хотите переписать.

Краткое руководство по принятию решений

Ответьте на эти вопросы, чтобы выбрать подход:

  1. Выполняется ли полная перезапись или добавочная миграция?

  2. У вас есть обширное использование HttpContext в общих библиотеках?

Сравнение подходов к миграции

Подход Изменения кода Производительность Общие библиотеки Когда следует использовать
Полная перезапись High — перезапись всего кода HttpContext Лучший Требуется обновление Полные перезаписи и приложения, критически важные для производительности
Адаптеры System.Web Низкая— сохранение существующих шаблонов Хорошо Работает с существующим кодом Добавочные миграции, широкое использование HttpContext

Важные различия

Время существования HttpContext

Адаптеры поддерживаются HttpContext, которые не могут использоваться после срока действия запроса. Таким образом, HttpContext при запуске на ASP.NET Core не может использоваться после выполнения запроса, в то время как на ASP.NET Framework он иногда работает. Будет выброшено ObjectDisposedException в случаях, когда оно используется после завершения выполнения запроса.

Рекомендация: Сохраните необходимые значения в POCO и храните их там.

Соображения по потокам в запросах

Предупреждение

ASP.NET Core не гарантирует сходство потоков для запросов. Если для кода требуется потокобезопасный доступ к HttpContext, необходимо обеспечить правильную синхронизацию.

В ASP.NET Framework запрос был привязан к потоку, и Current был бы доступен только на этом потоке. ASP.NET Core не имеет этой гарантии, поэтому Current будет доступен в том же асинхронном контексте, но никаких гарантий о потоках не даётся.

Рекомендация: при чтении и записи в HttpContext, необходимо убедиться, что вы делаете это в однопоточном режиме. Вы можете принудительно заставить запрос никогда не выполняться одновременно в любом асинхронном контексте, задав параметр ISingleThreadedRequestMetadata. Это будет иметь последствия для производительности и должно использоваться только если вы не можете изменить код, чтобы обеспечить несобместный доступ. Существует реализация, доступная для добавления в контроллеры с помощью SingleThreadedRequestAttribute:

[SingleThreadedRequest]
public class SomeController : Controller
{
    ...
} 

Буферизация потоков запроса

По умолчанию входящие запросы не всегда доступны и не полностью доступны. Чтобы получить поведение, наблюдаемое в .NET Framework, можно выбрать предварительную буферизацию входного потока. Это полностью считывает входящий поток и буферизирует его в память или диск (в зависимости от параметров).

Рекомендация. Это можно включить, применяя метаданные конечной IPreBufferRequestStreamMetadata точки, реализующие интерфейс. Это доступно как атрибут PreBufferRequestStreamAttribute , который можно применить к контроллерам или методам.

Чтобы включить это для всех конечных точек MVC, существует метод расширения, который можно использовать следующим образом:

app.MapDefaultControllerRoute()
    .PreBufferRequestStream();

Буферизация потока ответа

Для некоторых API Response требуется буферизация выходного потока, например Output, End(), Clear()и SuppressContent.

Рекомендация: Чтобы поддержать поведение для Response, которое требует буферизации ответа перед отправкой, конечные точки должны активировать эту функцию с помощью метаданных конечной точки, реализующих IBufferResponseStreamMetadata.

Чтобы включить это для всех конечных точек MVC, существует метод расширения, который можно использовать следующим образом:

app.MapDefaultControllerRoute()
    .BufferResponseStream();

Завершение перезаписи в ASP.NET Core HttpContext

Выберите этот подход при выполнении полной миграции и когда вы можете переписать код, связанный с HttpContext, для использования собственной реализации ASP.NET Core.

ASP.NET HttpContext Core обеспечивает более модульный и расширяемый дизайн по сравнению с ASP.NET Framework. Этот подход обеспечивает лучшую производительность, но требует больше изменений кода во время миграции.

Обзор

HttpContext значительно изменился в ASP.NET Core. При миграции модулей HTTP или обработчиков на промежуточное программное обеспечение необходимо обновить код для работы с новым HttpContext API.

В промежуточном слое ASP.NET Core метод Invoke принимает параметр типа HttpContext:

public async Task Invoke(HttpContext context)

Это HttpContext отличается от версии ASP.NET Framework и требует различных подходов к доступу к информации о запросах и ответах.

Переводы свойств

В этом разделе показано, как преобразовать наиболее часто используемые свойства System.Web.HttpContext в эквивалент Microsoft.AspNetCore.Http.HttpContext в ASP.NET Core.

Свойства HttpContext

  • HttpContext.ItemsHttpContext.Items

    IDictionary<object, object> items = httpContext.Items;
    
  • Нет эквивалентаHttpContext.TraceIdentifier

    string requestId = httpContext.TraceIdentifier;
    

    Уникальный идентификатор запроса для журналирования

Свойства HttpRequest

  • HttpRequest.HttpMethodHttpRequest.Method

    string httpMethod = httpContext.Request.Method;
    
  • HttpRequest.QueryStringHttpRequest.QueryString

    IQueryCollection queryParameters = httpContext.Request.Query;
    
    // If no query parameter "key" used, values will have 0 items
    // If single value used for a key (...?key=v1), values will have 1 item ("v1")
    // If key has multiple values (...?key=v1&key=v2), values will have 2 items ("v1" and "v2")
    IList<string> values = queryParameters["key"];
    
    // If no query parameter "key" used, value will be ""
    // If single value used for a key (...?key=v1), value will be "v1"
    // If key has multiple values (...?key=v1&key=v2), value will be "v1,v2"
    string value = queryParameters["key"].ToString();
    
  • HttpRequest.Url / HttpRequest.RawUrlнескольких свойств

    // using Microsoft.AspNetCore.Http.Extensions;
    var url = httpContext.Request.GetDisplayUrl();
    

    Использование Request.Scheme, Host, PathBase, Path, QueryString

  • HttpRequest.IsSecureConnectionHttpRequest.IsHttps

    var isSecureConnection = httpContext.Request.IsHttps;
    
  • HttpRequest.UserHostAddressConnectionInfo.RemoteIpAddress

    var userHostAddress = httpContext.Connection.RemoteIpAddress?.ToString();
    
  • HttpRequest.CookiesHttpRequest.Cookies

    IRequestCookieCollection cookies = httpContext.Request.Cookies;
    string unknownCookieValue = cookies["unknownCookie"]; // will be null (no exception)
    string knownCookieValue = cookies["cookie1name"];     // will be actual value
    
  • HttpRequest.RequestContextRoutingHttpContextExtensions.GetRouteData

    var routeValue = httpContext.GetRouteValue("key");
    
  • HttpRequest.HeadersHttpRequest.Headers

    // using Microsoft.AspNetCore.Http.Headers;
    // using Microsoft.Net.Http.Headers;
    
    IHeaderDictionary headersDictionary = httpContext.Request.Headers;
    
    // GetTypedHeaders extension method provides strongly typed access to many headers
    var requestHeaders = httpContext.Request.GetTypedHeaders();
    CacheControlHeaderValue cacheControlHeaderValue = requestHeaders.CacheControl;
    
    // For unknown header, unknownheaderValues has zero items and unknownheaderValue is ""
    IList<string> unknownheaderValues = headersDictionary["unknownheader"];
    string unknownheaderValue = headersDictionary["unknownheader"].ToString();
    
    // For known header, knownheaderValues has 1 item and knownheaderValue is the value
    IList<string> knownheaderValues = headersDictionary[HeaderNames.AcceptLanguage];
    string knownheaderValue = headersDictionary[HeaderNames.AcceptLanguage].ToString();
    
  • HttpRequest.UserAgentHttpRequest.Headers

    string userAgent = headersDictionary[HeaderNames.UserAgent].ToString();
    
  • HttpRequest.UrlReferrerHttpRequest.Headers

    string urlReferrer = headersDictionary[HeaderNames.Referer].ToString();
    
  • HttpRequest.ContentTypeHttpRequest.ContentType

    // using Microsoft.Net.Http.Headers;
    
    MediaTypeHeaderValue mediaHeaderValue = requestHeaders.ContentType;
    string contentType = mediaHeaderValue?.MediaType.ToString();   // ex. application/x-www-form-urlencoded
    string contentMainType = mediaHeaderValue?.Type.ToString();    // ex. application
    string contentSubType = mediaHeaderValue?.SubType.ToString();  // ex. x-www-form-urlencoded
    
    System.Text.Encoding requestEncoding = mediaHeaderValue?.Encoding;
    
  • HttpRequest.FormHttpRequest.Form

    if (httpContext.Request.HasFormContentType)
    {
        IFormCollection form;
    
        form = httpContext.Request.Form; // sync
        // Or
        form = await httpContext.Request.ReadFormAsync(); // async
    
        string firstName = form["firstname"];
        string lastName = form["lastname"];
    }
    

    Предупреждение: Читать значения формы следует только если тип контента x-www-form-urlencoded или form-data

  • HttpRequest.InputStreamHttpRequest.Body

    string inputBody;
    using (var reader = new System.IO.StreamReader(
        httpContext.Request.Body, System.Text.Encoding.UTF8))
    {
        inputBody = reader.ReadToEnd();
    }
    

    Предупреждение: Используйте только в промежуточных слоях обработчика в конце конвейера. Объект может быть прочитан только один раз за запрос

Свойства HttpResponse

  • HttpResponse.Status / HttpResponse.StatusDescriptionHttpResponse.StatusCode

    // using Microsoft.AspNetCore.Http;
    httpContext.Response.StatusCode = StatusCodes.Status200OK;
    
  • HttpResponse.ContentEncoding / HttpResponse.ContentTypeHttpResponse.ContentType

    // using Microsoft.Net.Http.Headers;
    var mediaType = new MediaTypeHeaderValue("application/json");
    mediaType.Encoding = System.Text.Encoding.UTF8;
    httpContext.Response.ContentType = mediaType.ToString();
    
  • HttpResponse.ContentTypeHttpResponse.ContentType

    httpContext.Response.ContentType = "text/html";
    
  • HttpResponse.OutputHttpResponseWritingExtensions.WriteAsync

    string responseContent = GetResponseContent();
    await httpContext.Response.WriteAsync(responseContent);
    
  • HttpResponse.TransmitFileСм. функции запроса

    Обслуживание файлов рассматривается в функциях запроса в ASP.NET Core

  • HttpResponse.HeadersHttpResponse.OnStarting

    // using Microsoft.AspNet.Http.Headers;
    // using Microsoft.Net.Http.Headers;
    
    private Task SetHeaders(object context)
    {
        var httpContext = (HttpContext)context;
    
        // Set header with single value
        httpContext.Response.Headers["ResponseHeaderName"] = "headerValue";
    
        // Set header with multiple values
        string[] responseHeaderValues = new string[] { "headerValue1", "headerValue1" };
        httpContext.Response.Headers["ResponseHeaderName"] = responseHeaderValues;
    
        // Translating ASP.NET 4's HttpContext.Response.RedirectLocation  
        httpContext.Response.Headers[HeaderNames.Location] = "http://www.example.com";
        // Or
        httpContext.Response.Redirect("http://www.example.com");
    
        // GetTypedHeaders extension method provides strongly typed access to many headers
        var responseHeaders = httpContext.Response.GetTypedHeaders();
    
        // Translating ASP.NET 4's HttpContext.Response.CacheControl 
        responseHeaders.CacheControl = new CacheControlHeaderValue
        {
            MaxAge = new System.TimeSpan(365, 0, 0, 0)
            // Many more properties available 
        };
    
        // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster
        return Task.FromResult(0);
    }
    

    Необходимо использовать шаблон обратного вызова для задания заголовков перед запуском ответа

  • HttpResponse.CookiesHttpResponse.OnStarting

    private Task SetCookies(object context)
    {
        var httpContext = (HttpContext)context;
    
        IResponseCookies responseCookies = httpContext.Response.Cookies;
    
        responseCookies.Append("cookie1name", "cookie1value");
        responseCookies.Append("cookie2name", "cookie2value",
            new CookieOptions { Expires = System.DateTime.Now.AddDays(5), HttpOnly = true });
    
        // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster
        return Task.FromResult(0); 
    }
    

    Необходимо использовать шаблон обратного вызова для задания файлов cookie перед запуском ответа

  • Настройка заголовков ответов:

    public async Task Invoke(HttpContext httpContext)
    {
        // Set callback to execute before response starts
        httpContext.Response.OnStarting(SetHeaders, state: httpContext);
        // ... rest of middleware logic
    }
    
  • Настройка файлов cookie ответа:

public async Task Invoke(HttpContext httpContext)
{
    // Set callbacks to execute before response starts
    httpContext.Response.OnStarting(SetCookies, state: httpContext);
    httpContext.Response.OnStarting(SetHeaders, state: httpContext);
    // ... rest of middleware logic
}

Адаптеры System.Web

Замечание

Это позволяет использовать системные веб-адаптеры для упрощения миграции.

Выберите этот подход при обширном использовании HttpContext в общих библиотеках или при добавочной миграции, в которой требуется свести к минимуму изменения кода.

Адаптеры System.Web предоставляют уровень совместимости, который позволяет использовать знакомые HttpContext API в приложениях ASP.NET Core. Этот подход особенно полезен, когда:

  • У вас есть общие библиотеки, использующие HttpContext
  • Вы выполняете добавочную миграцию
  • Вы хотите свести к минимуму изменения кода во время процесса миграции

Преимущества использования адаптеров System.Web

  • Минимальные изменения кода. Сохранение существующих System.Web.HttpContext шаблонов использования
  • Общие библиотеки: библиотеки могут работать как с ASP.NET Framework, так и с ASP.NET Core
  • Добавочная миграция. Перенос приложений по частям без нарушения общих зависимостей
  • Ускорение миграции. Сокращение времени, необходимого для миграции сложных приложений

Соображения

  • Производительность: хотя адаптеры работают хорошо, они вводят некоторые издержки по сравнению с родными API ASP.NET Core
  • Функциональное соответствие: Не все HttpContext возможности доступны через адаптеры
  • Долгосрочная стратегия. Рассмотрите возможность миграции в собственные api-интерфейсы ASP.NET Core для оптимальной производительности.

Дополнительные сведения о адаптерах System.Web см. в документации по адаптерам System.Web.

Дополнительные ресурсы