将 HTTP 模块迁移到 ASP.NET 核心中间件

本文介绍如何将现有 ASP.NET HTTP 模块从 system.webserver 迁移到 ASP.NET Core 中间件

重新访问的模块

在继续 ASP.NET 核心中间件之前,让我们先回顾一下 HTTP 模块的工作原理:

模块处理程序

模块包括:

  • 实现IHttpModule的类

  • 为每个请求调用

  • 能够中断(停止进一步处理请求)

  • 能够添加到 HTTP 响应,或创建自己的响应

  • 配置Web.config

模块处理传入请求的顺序由决定

  1. 由 ASP.NET(例如 BeginRequestAuthenticateRequest)触发的系列事件。 有关完整列表,请参阅 System.Web.HttpApplication。 每个模块都可以为一个或多个事件创建处理程序。

  2. 对于同一事件,它们在 Web.config中配置的顺序。

除了模块,还可以将生命周期事件的处理程序添加到 Global.asax.cs 文件中。 这些处理程序在已配置模块中的处理程序之后执行。

从模块到中间件

中间件比 HTTP 模块更简单:

  • 模块、 Global.asax.csWeb.config (IIS 配置除外)和应用程序生命周期消失

  • 模块的角色已被中间件接管

  • 中间件是使用代码而不是在 Web.config 中配置的

  • 通过管道分支 ,可以基于 URL 以及请求标头、查询字符串等将请求发送到特定中间件。
  • 通过管道分支 ,可以基于 URL 以及请求标头、查询字符串等将请求发送到特定中间件。

中间件与模块非常相似:

中间件和模块按不同的顺序进行处理:

授权中间件对未授权的用户的请求进行短路。MVC 中间件允许和处理索引页的请求。自定义报表中间件允许和处理销售报表的请求。

请注意上图中身份验证中间件如何对请求进行短路。

将模块代码迁移到中间件

现有的 HTTP 模块如下所示:

// ASP.NET 4 module

using System;
using System.Web;

namespace MyApp.Modules
{
    public class MyModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
            application.EndRequest += (new EventHandler(this.Application_EndRequest));
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the beginning of request processing.
        }

        private void Application_EndRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the end of request processing.
        }
    }
}

中间件页所示,ASP.NET Core 中间件是一个类,该类提供一个方法Invoke,此方法接受HttpContext并返回Task。 新中间件如下所示:

// ASP.NET Core middleware

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
    public class MyMiddleware
    {
        private readonly RequestDelegate _next;

        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            // Do something with context near the beginning of request processing.

            await _next.Invoke(context);

            // Clean up.
        }
    }

    public static class MyMiddlewareExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddleware>();
        }
    }
}

前面的中间件模板摘自 有关编写中间件的部分。

使用MyMiddlewareExtensions辅助类可以更轻松地在Startup类中配置中间件。 该方法 UseMyMiddleware 将中间件类添加到请求管道。 中间件所需的服务将注入中间件的构造函数中。

模块可能会终止请求,例如,如果用户未获得授权:

// ASP.NET 4 module that may terminate the request

private void Application_BeginRequest(Object source, EventArgs e)
{
    HttpContext context = ((HttpApplication)source).Context;

    // Do something with context near the beginning of request processing.

    if (TerminateRequest())
    {
        context.Response.End();
        return;
    }
}

中间件通过不调用 Invoke 管道中的下一个中间件来处理此问题。 请记住,这不会完全终止请求,因为当响应通过管道返回时,仍会调用以前的中间件。

// ASP.NET Core middleware that may terminate the request

public async Task Invoke(HttpContext context)
{
    // Do something with context near the beginning of request processing.

    if (!TerminateRequest())
        await _next.Invoke(context);

    // Clean up.
}

将模块的功能迁移到新中间件时,你可能会发现代码未编译,因为 HttpContext 该类在 ASP.NET Core 中发生了显著更改。 请参阅 从 ASP.NET Framework HttpContext 迁移到 ASP.NET Core ,了解如何迁移到新的 ASP.NET Core HttpContext。

将模块插入迁移到请求管道

HTTP 模块通常使用 Web.config添加到请求管道:

<?xml version="1.0" encoding="utf-8"?>
<!--ASP.NET 4 web.config-->
<configuration>
  <system.webServer>
    <modules>
      <add name="MyModule" type="MyApp.Modules.MyModule"/>
    </modules>
  </system.webServer>
</configuration>

通过将 新中间件添加到 类中的 Startup 请求管道来转换此值:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMyMiddleware();

    app.UseMyMiddlewareWithParams();

    var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
    var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
    app.UseMyMiddlewareWithParams(myMiddlewareOptions);
    app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

    app.UseMyTerminatingMiddleware();

    // Create branch to the MyHandlerMiddleware. 
    // All requests ending in .report will follow this branch.
    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".report"),
        appBranch => {
            // ... optionally add more middleware to this branch
            appBranch.UseMyHandler();
        });

    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".context"),
        appBranch => {
            appBranch.UseHttpContextDemoMiddleware();
        });

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

插入新中间件的管道中的确切位置取决于它作为模块(BeginRequestEndRequest等)处理的事件及其在 Web.config模块列表中的顺序。

如前所述,ASP.NET Core 中没有应用程序生命周期,中间件处理响应的顺序与模块使用的顺序不同。 这可能会使你的订购决策更具挑战性。

如果排序成为问题,可以将模块拆分为多个可以独立排序的中间件组件。

使用选项模式加载中间件选项

某些模块具有存储在 Web.config中的配置选项。但是,在 ASP.NET Core 中,将使用新的配置模型来代替 Web.config

新的 配置系统 提供了以下选项来解决此问题:

  1. 创建用于保存中间件选项的类,例如:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. 存储选项值

    配置系统允许在任意位置存储选项值。 但是,大多数网站都使用 appsettings.json,因此我们将采用以下方法:

    {
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    

    此处的 MyMiddlewareOptionsSection 是一个节名称。 它不必与选项类的名称相同。

  3. 将选项值与 options 类相关联

    选项模式使用 ASP.NET Core 的依赖项注入框架将选项类型(例如 MyMiddlewareOptions)与 MyMiddlewareOptions 具有实际选项的对象相关联。

    更新类 Startup

    1. 如果使用 appsettings.json,请在 Startup 构造函数中将其添加到配置生成器:

      public Startup(IHostingEnvironment env)
      {
          var builder = new ConfigurationBuilder()
              .SetBasePath(env.ContentRootPath)
              .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
              .AddEnvironmentVariables();
          Configuration = builder.Build();
      }
      
    2. 配置选项服务:

      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();
      
          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));
      
          // Add framework services.
          services.AddMvc();
      }
      
    3. 将选项与选项类绑定:

      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();
      
          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));
      
          // Add framework services.
          services.AddMvc();
      }
      
  4. 将选项注入中间件构造函数。 这类似于将选项注入控制器。

    public class MyMiddlewareWithParams
    {
        private readonly RequestDelegate _next;
        private readonly MyMiddlewareOptions _myMiddlewareOptions;
    
        public MyMiddlewareWithParams(RequestDelegate next,
            IOptions<MyMiddlewareOptions> optionsAccessor)
        {
            _next = next;
            _myMiddlewareOptions = optionsAccessor.Value;
        }
    
        public async Task Invoke(HttpContext context)
        {
            // Do something with context near the beginning of request processing
            // using configuration in _myMiddlewareOptions
    
            await _next.Invoke(context);
    
            // Do something with context near the end of request processing
            // using configuration in _myMiddlewareOptions
        }
    }
    

    将中间件添加到管道中的 UseMiddleware 扩展方法负责处理依赖项注入。

    这不限于 IOptions 对象。 可以通过这种方式注入中间件所需的任何其他对象。

通过直接注入的方式加载中间件选项

选项模式的优点是它在选项值与其使用者之间产生松散耦合。 将选项类与实际选项值相关联后,任何其他类都可以通过依赖项注入框架访问这些选项。 无需传递选项值。

如果想要使用同一中间件两次,但使用不同的选项,则这会中断。 例如,不同分支中使用的授权中间件允许不同的角色。 不能将两个不同的选项对象与一个选项类相关联。

解决方案是获取包含类中 Startup 实际选项值的选项对象,并将这些值直接传递给中间件的每个实例。

  1. 将第二个键添加到 appsettings.json

    若要向 appsettings.json 文件添加第二组选项,请使用新密钥来唯一标识它:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. 检索选项值并将其传递给中间件。 添加中间件到管道的扩展Use...方法是传入选项值的一个合理方法:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();
    
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
    
        app.UseMyMiddleware();
    
        app.UseMyMiddlewareWithParams();
    
        var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
        var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
        app.UseMyMiddlewareWithParams(myMiddlewareOptions);
        app.UseMyMiddlewareWithParams(myMiddlewareOptions2);
    
        app.UseMyTerminatingMiddleware();
    
        // Create branch to the MyHandlerMiddleware. 
        // All requests ending in .report will follow this branch.
        app.MapWhen(
            context => context.Request.Path.ToString().EndsWith(".report"),
            appBranch => {
                // ... optionally add more middleware to this branch
                appBranch.UseMyHandler();
            });
    
        app.MapWhen(
            context => context.Request.Path.ToString().EndsWith(".context"),
            appBranch => {
                appBranch.UseHttpContextDemoMiddleware();
            });
    
        app.UseStaticFiles();
    
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
    
  3. 启用中间件以接受 options 参数。 提供一个Use...扩展方法的重载版本(该版本接收options参数并将其传递给UseMiddleware)。 使用参数调用时 UseMiddleware ,它会在实例化中间件对象时将参数传递给中间件构造函数。

    public static class MyMiddlewareWithParamsExtensions
    {
        public static IApplicationBuilder UseMyMiddlewareWithParams(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddlewareWithParams>();
        }
    
        public static IApplicationBuilder UseMyMiddlewareWithParams(
            this IApplicationBuilder builder, MyMiddlewareOptions myMiddlewareOptions)
        {
            return builder.UseMiddleware<MyMiddlewareWithParams>(
                new OptionsWrapper<MyMiddlewareOptions>(myMiddlewareOptions));
        }
    }
    

    请注意这是如何将 options 对象包装进 OptionsWrapper 对象中的。 这将实现中间件构造函数所预期的IOptions

增量 IHttpModule 迁移

有时无法轻松地将模块转换为中间件。 为了支持需要模块且无法移动到中间件的迁移方案,System.Web 适配器支持将它们添加到 ASP.NET Core。

IHttpModule 示例

为了支持模块,必须提供一个HttpApplication实例。 如果未使用任何自定义HttpApplication,将使用默认选项来添加模块。 自定义应用程序(包括 Application_Start)中声明的事件将相应地注册并运行。

using System.Web;
using Microsoft.AspNetCore.OutputCaching;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<MyApp>(options =>
    {
        // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be
        options.PoolSize = 10;

        // Register a module (optionally) by name
        options.RegisterModule<MyModule>("MyModule");
    });

// Only available in .NET 7+
builder.Services.AddOutputCache(options =>
{
    options.AddHttpApplicationBasePolicy(_ => new[] { "browser" });
});

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();

app.UseSystemWebAdapters();
app.UseOutputCache();

app.MapGet("/", () => "Hello World!")
    .CacheOutput();

app.Run();

class MyApp : HttpApplication
{
    protected void Application_Start()
    {
    }

    public override string? GetVaryByCustomString(System.Web.HttpContext context, string custom)
    {
        // Any custom vary-by string needed

        return base.GetVaryByCustomString(context, custom);
    }
}

class MyModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += (s, e) =>
        {
            // Handle events at the beginning of a request
        };

        application.AuthorizeRequest += (s, e) =>
        {
            // Handle events that need to be authorized
        };
    }

    public void Dispose()
    {
    }
}

Global.asax 迁移

根据需要,此基础设施可用于迁移 Global.asax 的使用情况。 源自 Global.asaxHttpApplication 是自定义的,文件可以包含在 ASP.NET Core 应用程序中。 由于它已命名 Global,因此可以使用以下代码来注册它:

builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<Global>();

只要其中的逻辑在 ASP.NET Core 中可用,此方法就可以用于以增量方式将依赖 Global.asax 性迁移到 ASP.NET Core。

身份验证/授权事件

若要在所需时间运行身份验证和授权事件,应使用以下模式:

app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();

如果未完成此操作,事件仍将运行。 但是,它将在调用.UseSystemWebAdapters()的过程中。

HTTP 模块池化

由于 ASP.NET Framework 中的模块和应用程序已分配给请求,因此每个请求都需要一个新实例。 但是,由于创建成本高昂,因此使用 ObjectPool<T>池化。 若要自定义实例的实际生存期 HttpApplication ,可以使用自定义池:

builder.Services.TryAddSingleton<ObjectPool<HttpApplication>>(sp =>
{
    // Recommended to use the in-built policy as that will ensure everything is initialized correctly and is not intended to be replaced
    var policy = sp.GetRequiredService<IPooledObjectPolicy<HttpApplication>>();

    // Can use any provider needed
    var provider = new DefaultObjectPoolProvider();

    // Use the provider to create a custom pool that will then be used for the application.
    return provider.Create(policy);
});

其他资源