将 ASP.NET Framework HttpContext 迁移到 ASP.NET Core

HttpContext 是 Web 应用程序的基本组件,提供对 HTTP 请求和响应信息的访问权限。 从 ASP.NET Framework 迁移到 ASP.NET Core 时,HttpContext 提出了独特的挑战,因为两个框架具有不同的 API 和方法。

为什么 HttpContext 迁移很复杂

ASP.NET Framework 和 ASP.NET Core 具有基本不同的 HttpContext 实现:

这些差异意味着,您无法直接将 HttpContext 代码从 Framework 移动到 Core 而无需更改。

迁移策略概述

在迁移过程中,有两种处理 HttpContext 的主要方法:

  1. 完全重写 - 重写所有 HttpContext 代码以使用 ASP.NET Core 的本机 HttpContext 实现
  2. System.Web 适配器 - 使用适配器在以增量方式迁移时最大程度地减少代码更改

对于大多数应用程序,迁移到 ASP.NET Core 的本机 HttpContext 可提供最佳性能和可维护性。 但是,较大的应用程序或具有大量 HttpContext 用法的应用程序可能受益于在增量迁移期间使用 System.Web 适配器。

选择迁移方法

有两个主要选项用于将 HttpContext 从 ASP.NET Framework 迁移到 ASP.NET Core。 选择取决于迁移时间线、是否需要同时运行这两个应用程序,以及你愿意重写的代码量。

快速决策指南

回答以下问题以选择你的方法:

  1. 是否执行完全重写或增量迁移?

  2. 在多个共享库中是否广泛使用了 HttpContext?

迁移方法比较

方法 代码更改 性能 共享库 何时使用
完全重写 高 - 重写所有 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();

响应流缓冲

Response 上的一些 API 需要缓冲输出流,例如OutputEnd()Clear(),和SuppressContent

建议:为了支持要求在发送之前缓冲响应的 Response 行为,终结点必须通过实现 IBufferResponseStreamMetadata 的终结点元数据选择使用此功能。

若要在所有 MVC 终结点上启用此功能,可以使用如下扩展方法:

app.MapDefaultControllerRoute()
    .BufferResponseStream();

完全重写为 ASP.NET Core HttpContext

执行完整迁移时,请选择此方法,并可以重写与 HttpContext 相关的代码以使用 ASP.NET Core 的本机实现。

与 ASP.NET Framework 相比,ASP.NET Core 的 HttpContext 提供了更模块化和可扩展的设计。 此方法提供最佳性能,但在迁移过程中需要更多代码更改。

概述

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属性翻译为 ASP.NET Core 中的等效Microsoft.AspNetCore.Http.HttpContext

HttpContext 属性

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-urlencodedform-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); 
    }
    

    必须在响应启动之前使用回调模式来设置Cookies

  • 设置响应标头:

    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 适配器

注释

这利用 System.Web 适配器 来简化迁移。

在共享库之间使用广泛的 HttpContext 或执行增量迁移时,如果要最大程度地减少代码更改,请选择此方法。

System.Web 适配器提供了一个兼容性层,使你可以在 ASP.NET Core 应用程序中使用熟悉HttpContext的 API。 此方法在以下情况下特别有用:

  • 你有使用 HttpContext 的共享库
  • 正在执行增量迁移
  • 希望在迁移过程中最大程度地减少代码更改

使用 System.Web 适配器的好处

  • 最少的代码更改:保留现有 System.Web.HttpContext 使用模式
  • 共享库:库可与 ASP.NET Framework 和 ASP.NET Core 配合使用
  • 增量迁移:逐条迁移应用程序,而不会中断共享依赖项
  • 更快的迁移:缩短迁移复杂应用程序所需的时间

注意事项

  • 性能:虽然表现不错,但与原生的 ASP.NET Core API 相比,适配器引入了一些额外的开销。
  • 功能对等:并非所有 HttpContext 功能都通过适配器提供
  • 长期策略:考虑最终迁移到本机 ASP.NET 核心 API,以获得最佳性能

有关 System.Web 适配器的详细信息,请参阅 System.Web 适配器文档

其他资源