中间件是一种装配到应用管道以处理请求和响应的组件,每个组件:

  • 选择是否将请求传递到管道中的下一个组件
  • 可在管道中的下一个组件前后执行工作

  请求委托用于生成请求管道。请求委托处理每个HTTP请求。
  使用RunMapUse扩展方法来配置请求委托。可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。这些可重用的类和并行匿名方法即为中间件,也叫做中间件组件。请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。当中间件短路时,它被称为“终端中间件”,因为它阻止中间件进一步处理请求。

  ASP.NET Core 请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。

通过以下示例观察中间件的执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async(context,next)=>
{
await context.Response.WriteAsync("Middleware 1 before\n");
await next(context);
await context.Response.WriteAsync("Middleware 1 end\n");
});

app.Use(async(context,next)=>
{
await context.Response.WriteAsync("Middleware 2 before\n");
await next(context);
await context.Response.WriteAsync("Middleware 2 end\n");
});

app.Use(async(context,next)=>
{
await context.Response.WriteAsync("Middleware 3 before\n");
await next(context);
await context.Response.WriteAsync("Middleware 3 end\n");
});

app.Run();

  每个委托均可在下一个委托前后执行操作。应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

Use委托

  用Use将多个请求委托链接在一起。next参数表示管道中的下一个委托。可通过不调用next参数使管道短路。通常可在next委托前后执行操作,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();

Run委托

  Run委托不会收到next参数。第一个Run委托始终为终端,用于终止管道。Run时一种约定。某些中间件组件可能会公开在管道末尾运行的*Run[Middleware]***方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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(async context =>
{
await context.Response.WriteAsync("Hello from 3nd delegate.");
});

app.Run();

  在上面的示例中,Run 委托将 “Hello from 2nd delegate.” 写入响应,然后终止管道。 如果在 Run 委托之后添加了另一个 UseRun 委托,则不会调用该委托。

Map委托

  Map扩展用作约定来创建管道分支。Map基于给定请求路径的匹配项来创建请求管道分支。如果请求路径以给定路径开头,则执行分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1",app=>
{
app.Run(async context=>
{
await context.Response.WriteAsync("Map Test 1");
});
});

app.Map("/map2",app=>
{
app.Run(async context=>
{
await context.Response.WriteAsync("Map Test 2");
});
});

app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});

  下表使用前面的代码显示来自 http://localhost:1234 的请求和响应。

请求 响应
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

  使用Map时,将从HttpRequest.Path中删除匹配的路径段,并针对每个请求将该路径段追加到HttpRequest.PathBase

Map嵌套

1
2
3
4
5
6
7
8
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});

Map匹配多段

1
2
3
app.Map("/map1/seg1", app => {

});

MapWhen委托

  MapWhen 基于给定谓词的结果创建请求管道分支。 Func<HttpContext, bool> 类型的任何谓词均可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量 branch 是否存在:

1
2
3
4
5
6
7
8
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), 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

中间件顺序

  下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。 可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。

  向 Program.cs 文件中添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。

  Program.cs 中的以下突出显示的代码按照典型的建议顺序增加与安全相关的中间件组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error"); // 异常处理中间件
app.UseHsts(); // HTTP严格传输安全协议(HSTS)中间件
}

app.UseHttpsRedirection(); // HTTPS 重定向中间件
app.UseStaticFiles(); // 静态文件中间件
// app.UseCookiePolicy(); // Cookie 策略中间件

app.UseRouting(); // 用于路由请求的路由中间件
// app.UseRateLimiter(); // 限流中间件
// app.UseRequestLocalization(); // 检查请求区域性的任何中间件
// app.UseCors(); // 跨域处理中间件

app.UseAuthentication(); // 身份验证中间
app.UseAuthorization(); // 授权用户访问安全资源的授权中间件
// app.UseSession(); // 会话中间件
// app.UseResponseCompression(); // 压缩排序中间件
// app.UseResponseCaching(); // 高速缓存中间件

app.MapRazorPages(); //Razor Pages 终结点路由中间件
app.MapDefaultControllerRoute();

app.Run();

  在上述代码中,并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。例如:

  • UseCorsUseAuthenticationUseAuthorization 必须按显示的顺序出现。
  • UseCors 当前必须在 UseResponseCaching 之前出现。
  • UseRequestLocalization 必须在可能检查请求区域性的任何中间件(例如 app.UseStaticFiles())之前出现。
  • 在使用速率限制终结点特定的 API 时,必须在 UseRouting 之后调用 UseRateLimiter。 例如,如果使用 [EnableRateLimiting] 属性,则必须在 UseRouting 之后调用 UseRateLimiter。 当仅调用全局限制器时,可以在 UseRouting 之前调用 UseRateLimiter。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();

  以上代码将为常见应用场景添加中间件组件:

  1. 异常/错误处理
    • 当应用在开发环境中运行时
      • 开发人员异常页中间件(UseDevelopmentExceptionPage)报告应用运行时错误
      • 数据库错误页中间件(UseDatabaseErrorPage)报告数据库运行时错误
    • 当应用在生产环境中运行时
      • 异常处理程序中间件(UseExceptionHandler)捕获以下中间件中引发的异常
      • HTTP严格传输安全协议(HSTS)中间件(UseHsts)添加Strict-Transport-Security标头
  2. HTTPS重定向中间件(UseHttpsRedirection)将HTTP请求重定向到HTTPS
  3. 静态文件中间件(UseStaticFiles)返回静态文件,并简化进一步请求处理
  4. Cookie策略中间件(UseCookiePolicy)使应用符合欧盟一般数据保护条例(GDPR)规定
  5. 用于路由请求的路由中间件(UseRouting)
  6. 身份验证中间件(UseAuthentication)尝试对用户进行身份验证,然后才会允许用户访问安全资源
  7. 用于授权用户访问安全资源的授权中间件(UseAuthorization)
  8. 会话中间件(UseSession)建立和维护会话状态。如果应用使用会话状态,请在Cookie策略中间件之后和MVC中间件之前调用会话中间件
  9. 用于将 Razor Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPagesUseEndpoints)。

内置中间件

  ASP.NET Core 附带以下中间件组件。 “顺序”列提供备注,以说明中间件在请求处理管道中的放置,以及中间件可能会终止请求处理的条件。 如果中间件让请求处理管道短路,并阻止下游中间件进一步处理请求,它被称为“终端中间件”。