中间件是组装到应用程序管道中以处理请求和响应的软件。每个组件:
请求委托用于构建请求管道。请求委托处理每个 HTTP 请求。
请求委托是使用Run、Map和Use扩展方法配置的。单个请求委托可以内联指定为匿名方法(称为内联中间件),也可以在可重用类中定义。这些可重用的类和内联匿名方法就是中间件,也称为中间件组件。请求管道中的每个中间件组件负责调用管道中的下一个组件或短路管道。当中间件短路时,它被称为终端中间件,因为它会阻止其他中间件处理请求。
将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件解释了 ASP.NET Core 和 ASP.NET 4.x 中请求管道之间的差异,并提供了其他中间件示例。
ASP.NET Core 包含许多编译器平台分析器,用于检查应用程序代码的质量。有关详细信息,请参阅ASP.NET Core 应用中的代码分析
WebApplication
ASP.NET Core 请求管道由一系列请求委托组成,依次调用。下图演示了这个概念。执行线程遵循黑色箭头。
每个委托可以在下一个委托之前和之后执行操作。异常处理委托应在管道的早期调用,以便它们可以捕获管道后期发生的异常。
最简单的 ASP.NET Core 应用程序设置一个处理所有请求的请求委托。这种情况不包括实际的请求管道。相反,会调用单个匿名函数来响应每个 HTTP 请求。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello world!");
});
app.Run();
使用 Use将多个请求委托链接在一起。该next
参数代表管道中的下一个委托。您可以通过不调用参数来短路管道next
。您通常可以在委托之前和之后执行操作next
,如以下示例所示:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
app.Run();
当一个委托不将请求传递给下一个委托时,称为短路请求管道。短路通常是可取的,因为它可以避免不必要的工作。例如,静态文件中间件可以通过处理静态文件的请求并短路管道的其余部分来充当终端中间件。在终止进一步处理的中间件之前添加到管道的中间件仍然处理其next.Invoke
语句之后的代码。但是,请参阅以下有关尝试写入已发送的响应的警告。
警告
next.Invoke
响应发送给客户后请勿致电。响应开始后对HttpResponse的更改会引发异常。例如,设置标头和状态代码会引发异常。调用后写入响应正文next
:
Content-Length
。HasStarted是一个有用的提示,用于指示标头是否已发送或正文是否已写入。
运行委托不接收next
参数。第一个Run
委托始终是终端并终止管道。Run
是一个约定。某些中间件组件可能会公开Run[Middleware]
在管道末尾运行的方法:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
app.Run();
如果您希望将代码注释翻译成英语以外的语言,请在此 GitHub 讨论问题中告知我们。
在前面的示例中,Run
委托写入"Hello from 2nd delegate."
响应,然后终止管道。如果在委托之后添加另一个Use
或委托,则不会调用它。Run
Run
非分配app.Use扩展方法:
next
.有关更多信息,请参阅此 GitHub 问题。
下图显示了 ASP.NET Core MVC 和 Razor Pages 应用程序的完整请求处理管道。您可以看到在典型应用程序中现有中间件的排序方式以及自定义中间件的添加位置。您可以完全控制如何根据您的方案重新排序现有中间件或注入新的自定义中间件。
上图中的端点中间件执行相应应用程序类型(MVC 或 Razor Pages)的过滤器管道。
上图中的路由中间件显示在静态文件后面。这是项目模板通过显式调用app.UseRouting实现的顺序。如果不调用app.UseRouting
,路由中间件默认在管道的开头运行。有关详细信息,请参阅路由。
中间件组件在文件中添加的顺序Program.cs
定义了请求时调用中间件组件的顺序以及响应的相反顺序。该顺序对于安全性、性能和功能至关重要。
以下突出显示的代码Program.cs
按典型推荐顺序添加了与安全相关的中间件组件:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapDefaultControllerRoute();
app.Run();
在前面的代码中:
UseCors
、UseAuthentication
、 和UseAuthorization
必须按所示顺序出现。UseCors
当前必须出现在 之前UseResponseCaching
。此要求在GitHub 问题 dotnet/aspnetcore #23218中进行了解释。UseRequestLocalization
必须出现在任何可能检查请求区域性的中间件之前,例如app.UseStaticFiles()
.UseRouting
使用速率限制端点特定 API 后,必须调用UseRateLimiter 。例如,如果[EnableRateLimiting]使用该属性,则UseRateLimiter
必须在 后调用UseRouting
。仅调用全局限制器时,UseRateLimiter
可以在 之前调用UseRouting
。在某些场景下,中间件有不同的顺序。例如,缓存和压缩排序是特定于场景的,并且存在多个有效排序。例如:
app.UseResponseCaching();
app.UseResponseCompression();
使用前面的代码,可以通过缓存压缩响应来减少 CPU 使用率,但最终可能会使用不同的压缩算法(例如 Gzip 或 Brotli)缓存资源的多种表示形式。
以下顺序组合静态文件以允许缓存压缩的静态文件:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
以下Program.cs
代码为常见应用场景添加了中间件组件:
Strict-Transport-Security
标头。if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
在前面的示例代码中,每个中间件扩展方法都通过Microsoft.AspNetCore.Builder命名空间在WebApplicationBuilder上公开。
UseExceptionHandler是添加到管道中的第一个中间件组件。因此,异常处理程序中间件会捕获后续调用中发生的任何异常。
静态文件中间件在管道的早期被调用,以便它可以处理请求和短路,而无需通过其余组件。静态文件中间件不提供授权检查。静态文件中间件提供的任何文件(包括wwwroot下的文件)都是公开可用的。有关保护静态文件的方法,请参阅ASP.NET Core 中的静态文件。
如果静态文件中间件未处理该请求,则会将其传递到身份验证中间件 ( UseAuthentication ),后者执行身份验证。身份验证不会短路未经身份验证的请求。尽管身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor 页面或 MVC 控制器和操作后才会发生授权(和拒绝)。
以下示例演示了一个中间件顺序,其中静态文件请求先由静态文件中间件处理,然后再由响应压缩中间件处理。静态文件不会使用此中间件顺序进行压缩。Razor Pages 响应可以被压缩。
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
有关单页应用程序的信息,请参阅React和Angular项目模板指南。
UseCors
调用和 的顺序UseStaticFiles
取决于应用程序。有关详细信息,请参阅UseCors 和 UseStaticFiles 顺序
转发标头中间件应在其他中间件之前运行。此顺序确保依赖转发标头信息的中间件可以使用标头值进行处理。要在诊断和错误处理中间件之后运行转发标头中间件,请参阅转发标头中间件顺序。
映射扩展用作管道分支的约定。Map
根据给定请求路径的匹配对请求管道进行分支。如果请求路径以给定路径开头,则执行分支。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
http://localhost:1234
下表显示了使用上述代码的请求和响应。
要求 | 回复 |
---|---|
本地主机:1234 | 非地图代表您好。 |
本地主机:1234/map1 | 地图测试1 |
本地主机:1234/map2 | 地图测试2 |
本地主机:1234/map3 | 非地图代表您好。 |
使用时,将为每个请求Map
删除HttpRequest.Path
或附加匹配的路径段。HttpRequest.PathBase
Map
支持嵌套,例如:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
Map
也可以一次匹配多个段:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1/seg1", HandleMultiSeg);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
MapWhen根据给定谓词的结果对请求管道进行分支。任何类型的谓词Func
都可用于将请求映射到管道的新分支。在以下示例中,谓词用于检测查询字符串变量是否存在branch
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
http://localhost:1234
下表显示了使用先前代码的请求和响应:
要求 | 回复 |
---|---|
localhost:1234 |
Hello from non-Map delegate. |
localhost:1234/?branch=main |
Branch used = main |
UseWhen还根据给定谓词的结果对请求管道进行分支。与 with 不同MapWhen
,如果该分支没有短路或包含终端中间件,则它会重新加入主管道:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
appBuilder => HandleBranchAndRejoin(appBuilder));
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
void HandleBranchAndRejoin(IApplicationBuilder app)
{
var logger = app.ApplicationServices.GetRequiredService>();
app.Use(async (context, next) =>
{
var branchVer = context.Request.Query["branch"];
logger.LogInformation("Branch used = {branchVer}", branchVer);
// Do work that doesn't write to the Response.
await next();
// Do other work that doesn't write to the Response.
});
}
在前面的示例中,Hello from non-Map delegate.
为所有请求写入响应。如果请求包含查询字符串变量branch
,则在重新加入主管道之前记录其值。