中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:
请求委托用于生成请求管道。 请求委托处理每个 HTTP
请求。
使用 Run
,Map
和 Use
扩展方法来配置请求委托。 可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。 这些可重用的类和并行匿名方法即为中间件,也叫中间件组件。 请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。
将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件
介绍了 ASP.NET Core
和 ASP.NET 4.x
中请求管道之间的差异,并提供了更多的中间件示例。
IApplicationBuilder
创建中间件管道ASP.NET Core
请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。
每个委托均可在下一个委托前后执行操作。 此外,委托还可以决定不将请求传递给下一个委托,这就是对请求管道进行短路。 通常需要短路,因为这样可以避免不必要的工作。 例如,静态文件中间件可以返回静态文件请求并使管道的其余部分短路。 先在管道中调用异常处理委托,以便它们可以捕获在管道的后期阶段所发生的异常。
尽可能简单的 ASP.NET Core 应用设置了处理所有请求的单个请求委托。 这种情况不包括实际请求管道。 调用单个匿名函数以响应每个 HTTP 请求。
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
第一个 Run
委托终止了管道。
用Use
将多个请求委托链接在一起。 next
参数表示管道中的下一个委托。 可通过不调用 next
参数使管道短路。 通常可在下一个委托前后执行操作,如以下示例所示:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't 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.");
});
}
}
警告
在向客户端发送响应后,请勿调用next.Invoke
。 响应启动后,针对HttpResponse
的更改将引发异常。 例如,设置标头和状态代码更改将引发异常。 调用next
后写入响应正文:
- 可能导致违反协议。 例如,写入的长度超过规定的
Content-Length
。- 可能损坏正文格式。 例如,向 CSS 文件中写入 HTML 页脚。
HasStarted
是一个有用的提示,指示是否已发送标头或已写入正文。
向 Startup.Configure
方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此排序对于安全性、性能和功能至关重要。
以下 Startup.Configure
方法将为常见应用方案添加中间件组件:
public void Configure(IApplicationBuilder app)
{
if (env.IsDevelopment())
{
// When the app runs in the Development environment:
// Use the Developer Exception Page to report app runtime errors.
// Use the Database Error Page to report database runtime errors.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
// When the app doesn't run in the Development environment:
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
// Use the HTTP Strict Transport Security Protocol (HSTS)
// Middleware.
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Use HTTPS Redirection Middleware to redirect HTTP requests to HTTPS.
app.UseHttpsRedirection();
// Return static files and end the pipeline.
app.UseStaticFiles();
// Use Cookie Policy Middleware to conform to EU General Data
// Protection Regulation (GDPR) regulations.
app.UseCookiePolicy();
// Authenticate before the user accesses secure resources.
app.UseAuthentication();
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
app.UseSession();
// Add MVC to the request pipeline.
app.UseMvc();
}
在前面的示例代码中,每个中间件扩展方法都通过Microsoft.AspNetCore.Builder
命名空间在IApplicationBuilder
上公开。
UseExceptionHandler
是添加到管道的第一个中间件组件。 因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。
尽早在管道中调用静态文件中间件,以便它可以处理请求并使其短路,而无需通过剩余组件。 静态文件中间件不提供授权检查。 可公开访问由静态文件中间件服务的任何文件,包括 wwwroot
下的文件。 若要了解如何保护静态文件,请参阅 ASP.NET Core 中的静态文件
。
如果静态文件中间件未处理请求,则请求将被传递给执行身份验证的身份验证中间件 (UseAuthentication
)。 身份验证不使未经身份验证的请求短路。 虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor 页或 MVC 控制器和操作后,才发生授权(和拒绝)。
以下示例演示中间件排序,其中静态文件的请求在响应压缩中间件前由静态文件中间件进行处理。 使用此中间件顺序不压缩静态文件。 可以压缩来自 UseMvcWithDefaultRoute
的 MVC 响应。
public void Configure(IApplicationBuilder app)
{
// Static files not compressed by Static File Middleware.
app.UseStaticFiles();
app.UseResponseCompression();
app.UseMvcWithDefaultRoute();
}
使用 Use
、Run
和 Map
配置 HTTP 管道。 Use
方法可使管道短路(即不调用 next
请求委托)。 Run
是一种约定,并且某些中间件组件可公开在管道末尾运行的 Run[Middleware]
方法。
Map
扩展用作约定来创建管道分支。 Map*
基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。
public class Startup
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
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
。
MapWhen
基于给定谓词的结果创建请求管道分支。 Func
类型的任何谓词均可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量 branch
是否存在:
public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);
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/?branch=master | Branch used = master |
Map
支持嵌套,例如:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
此外,Map
还可同时匹配多个段:
public class Startup
{
private static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map multiple segments.");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1/seg1", HandleMultiSeg);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}
ASP.NET Core
附带以下中间件组件。 顺序列提供备注,说明中间件在请求管道中的放置,以及中间件可能终止请求并阻止其他中间件处理请求的条件。
中间件 | 描述 | 顺序 |
---|---|---|
身份验证 | 提供身份验证支持。 | 在需要 HttpContext.User 之前。 OAuth 回叫的终端。 |
Cookie 策略 | 跟踪用户是否同意存储个人信息,并强制实施 cookie 字段(如 secure 和 SameSite)的最低标准。 | 在发出 cookie 的中间件之前。 示例:身份验证、会话、MVC (TempData)。 |
CORS | 配置跨域资源共享。 | 在使用 CORS 的组件之前。 |
诊断 | 配置诊断。 | 在生成错误的组件之前。 |
转接头 | 将代理标头转发到当前请求。 | 在使用已更新字段的组件之前。 示例:方案、主机、客户端 IP、方法。 |
运行状况检查 | 检查 ASP.NET Core 应用及其依赖项的运行状况,如检查数据库可用性。 | 如果请求与运行状况检查终结点匹配,则为终端。 |
HTTP 方法重写 | 允许传入 POST 请求重写方法。 | 在使用已更新方法的组件之前。 |
HTTPS 重定向 | 将所有 HTTP 请求重定向到 HTTPS(ASP.NET Core 2.1 或更高版本)。 | 在使用 URL 的组件之前。 |
HTTP 严格传输安全性 (HSTS) | 添加特殊响应标头的安全增强中间件(ASP.NET Core 2.1 或更高版本)。 | 在发送响应之前,修改请求的组件之后。 示例:转接头、URL 重写。 |
MVC | 用 MVC/Razor Pages 处理请求(ASP.NET Core 2.0 或更高版本)。 | 如果请求与路由匹配,则为终端。 |
OWIN | 与基于 OWIN 的应用、服务器和中间件进行互操作。 | 如果 OWIN 中间件处理完请求,则为终端。 |
响应缓存 | 提供对缓存响应的支持。 | 在需要缓存的组件之前。 |
响应压缩 | 提供对压缩响应的支持。 | 在需要压缩的组件之前。 |
请求本地化 | 提供本地化支持。 | 在对本地化敏感的组件之前。 |
路由 | 定义和约束请求路由。 | 用于匹配路由的终端。 |
会话 | 提供对管理用户会话的支持。 | 在需要会话的组件之前。 |
静态文件 | 为提供静态文件和目录浏览提供支持。 | 如果请求与文件匹配,则为终端。 |
URL 重写 | 提供对重写 URL 和重定向请求的支持。 | 在使用 URL 的组件之前。 |
WebSockets | 启用 WebSockets 协议。 | 在接受 WebSocket 请求所需的组件之前。 |
通常,中间件封装在类中,并且通过扩展方法公开。 请考虑以下中间件,该中间件通过查询字符串设置当前请求的区域性:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
return next();
});
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
以上示例代码用于演示创建中间件组件。 有关 ASP.NET Core 的内置本地化支持,请参阅 ASP.NET Core 全球化和本地化
。
可通过传入区域性(如 http://localhost:7997/?culture=no
)测试中间件。
以下代码将中间件委托移动到类:
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
await _next(context);
}
}
}
以下扩展方法通过 IApplicationBuilder
公开中间件:
using Microsoft.AspNetCore.Builder;
namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}
以下代码通过 Startup.Configure
调用中间件:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture();
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
中间件应通过在其构造函数中公开其依赖项来遵循显式依赖项原则
。 在每个应用程序生存期构造一次中间件。 如果需要与请求中的中间件共享服务,请参阅按请求依赖项
部分。
中间件组件可通过构造函数参数从依赖关系注入 (DI)
解析其依赖项。UseMiddleware
也可直接接受其他参数。
由于中间件是在应用启动时构造的,而不是按请求构造的,因此在每个请求过程中,中间件构造函数使用的范围内生存期服务不与其他依赖关系注入类型共享。 如果必须在中间件和其他类型之间共享范围内服务,请将这些服务添加到 Invoke
方法的签名。 Invoke
方法可接受由 DI 填充的其他参数:
public class CustomMiddleware
{
private readonly RequestDelegate _next;
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
// IMyScopedService is injected into Invoke
public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty = 1000;
await _next(httpContext);
}
}