本文介绍如何将现有 ASP.NET HTTP 模块从 system.webserver 迁移到 ASP.NET Core 中间件。
重新访问的模块
在继续 ASP.NET 核心中间件之前,让我们先回顾一下 HTTP 模块的工作原理:
模块包括:
实现IHttpModule的类
为每个请求调用
能够中断(停止进一步处理请求)
能够添加到 HTTP 响应,或创建自己的响应
配置 在 Web.config 中
模块处理传入请求的顺序由决定
由 ASP.NET(例如 BeginRequest 和 AuthenticateRequest)触发的系列事件。 有关完整列表,请参阅 System.Web.HttpApplication。 每个模块都可以为一个或多个事件创建处理程序。
对于同一事件,它们在 Web.config中配置的顺序。
除了模块,还可以将生命周期事件的处理程序添加到 Global.asax.cs 文件中。 这些处理程序在已配置模块中的处理程序之后执行。
从模块到中间件
中间件比 HTTP 模块更简单:
模块、
Global.asax.csWeb.config (IIS 配置除外)和应用程序生命周期消失模块的角色已被中间件接管
中间件是使用代码而不是在 Web.config 中配置的
- 通过管道分支 ,可以基于 URL 以及请求标头、查询字符串等将请求发送到特定中间件。
- 通过管道分支 ,可以基于 URL 以及请求标头、查询字符串等将请求发送到特定中间件。
中间件与模块非常相似:
原则上每个请求都会被调用
通过不向下一个中间件传递请求来实现请求短路
能够创建自己的 HTTP 响应
中间件和模块按不同的顺序进行处理:
中间件的顺序基于它们插入请求管道的顺序,而模块的顺序主要基于 System.Web.HttpApplication 事件。
响应的中间件顺序与请求的顺序相反,而模块的顺序与请求和响应的顺序相同
请注意上图中身份验证中间件如何对请求进行短路。
将模块代码迁移到中间件
现有的 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。
新的 配置系统 提供了以下选项来解决此问题:
创建用于保存中间件选项的类,例如:
public class MyMiddlewareOptions { public string Param1 { get; set; } public string Param2 { get; set; } }存储选项值
配置系统允许在任意位置存储选项值。 但是,大多数网站都使用
appsettings.json,因此我们将采用以下方法:{ "MyMiddlewareOptionsSection": { "Param1": "Param1Value", "Param2": "Param2Value" } }此处的 MyMiddlewareOptionsSection 是一个节名称。 它不必与选项类的名称相同。
将选项值与 options 类相关联
选项模式使用 ASP.NET Core 的依赖项注入框架将选项类型(例如
MyMiddlewareOptions)与MyMiddlewareOptions具有实际选项的对象相关联。更新类
Startup:如果使用
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(); }配置选项服务:
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(); }将选项与选项类绑定:
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(); }
将选项注入中间件构造函数。 这类似于将选项注入控制器。
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 实际选项值的选项对象,并将这些值直接传递给中间件的每个实例。
将第二个键添加到
appsettings.json若要向
appsettings.json文件添加第二组选项,请使用新密钥来唯一标识它:{ "MyMiddlewareOptionsSection2": { "Param1": "Param1Value2", "Param2": "Param2Value2" }, "MyMiddlewareOptionsSection": { "Param1": "Param1Value", "Param2": "Param2Value" } }检索选项值并将其传递给中间件。 添加中间件到管道的扩展
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?}"); }); }启用中间件以接受 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.asax 的 HttpApplication 是自定义的,文件可以包含在 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);
});