(4)ASP.NET Core 中间件 1. 前言
中间件 (middleware) 是一种装配到应用管道以处理请求和响应的组件. 每个组件:
可选择是否将请求传递到管道中的下一个组件.
可在管道中的下一个组件前后执行工作.
2. 使用 IApplicationBuilder 创建中间件管道
ASP.NET Core 请求管道包含一系列请求委托, 依次调用. 下图演示了这一概念. 沿黑色箭头执行.
每个委托 (中间件) 均可在下一个委托前后执行操作. 任何委托都能选择停止传递到下一个委托, 转而自己处理该请求, 这就是请求管道的短路 (下面会举例说明). 而且是一种有意义的设计, 因为它可以避免不必要的工作. 比如, 一个授权(authorization) 中间件只有通过身份验证之后才能调用下一个委托, 否则它就会被短路, 并返回 "Not Authorized" 的响应. 所以应尽早在管道中调用异常处理委托, 这样它们就能捕获在管道的后期阶段发生的异常.
- public class Startup
- {
- public void Configure(IApplicationBuilder App)
- {
- App.Run(async context =>
- {
- await context.Response.WriteAsync("Hello, World!");
- });
- }
- }
响应结果:
由上面我们可以看到, 运行时输出的是 Run 委托消息, 然后我们再定义多一个请求委托看看效果, 请看如下代码:<喎"/kf/ware/vc/" target="_blank" class="keylink">vcD4KCjxwcmUgY2xhc3M9"brush:java;">public void Configure(IApplicationBuilder App) { // 第一个委托 Run App.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); // 第二个委托 Run App.Run(async context => { await context.Response.WriteAsync("Hey, World!"); }); }
响应结果:
由上述代码可以看到, 我们定义两个 Run 委托, 但是运行第一个 Run 委托的时候就已经终止了管道, 这是为什么呢?
短路管道(它不会调用 next 请求委托). 因此, Run 方法一般在管道尾部被调用. Run 是一种约定, 有些中间件组件可能会暴露他们自己的 Run 方法, 而这些方法只能在管道末尾处运行.
让我们再来看看如下代码:
- public void Configure(IApplicationBuilder App)
- {
- App.Use(async (context, next) =>
- {
- context.Response.ContentType = "text/plain; charset=utf-8";
- await context.Response.WriteAsync("进入第一个委托 执行下一个委托之前 \ r\n");
- // 调用管道中的下一个委托
- await next.Invoke();
- await context.Response.WriteAsync("结束第一个委托 执行下一个委托之后 \ r\n");
- });
- App.Run(async context =>
- {
- await context.Response.WriteAsync("进入第二个委托 \ r\n");
- await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
- await context.Response.WriteAsync("结束第二个委托 \ r\n");
- });
- }
响应结果:
通过响应结果, 我们可以看到 Use 方法将多个请求委托链接在一起. 而 next 参数表示管道中的下一个委托. 可通过不调用 next 参数使管道短路, 通常可在下一个委托前后执行操作.
3. 顺序
向 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(); } View Code
从上述示例代码中, 每个中间件扩展方法都通过 Microsoft.AspNetCore.Builder 命名空间在 IApplicationBuilder 上公开. 但是为什么我们要按照这个顺序去添加中间件组件呢? 下面我们挑几个中间件来了解下.
UseExceptionHandler(异常 / 错误处理)是添加到管道的第一个中间件组件. 因此我们可以捕获在应用程序调用中发生的任何异常. 那为什么要将异常 / 错误处理放在第一位呢? 那是因为这样我们就不用担心因前面中间件短路而导致捕获不到整个应用程序所有异常信息.
UseStaticFiles(静态文件)中间件在管道中提前调用, 方便它可以处理请求和短路, 而无需通过剩余中间组件. 也就是说静态文件中间件不用经过 UseAuthentication(身份验证)检查就可以直接访问, 即可公开访问由静态文件中间件服务的任何文件, 包括 wwwroot 下的文件.
UseAuthentication(身份验证)仅在 MVC 选择特定的 Razor 页面或 Controller 和 Action 之后才会发生.
- public void Configure(IApplicationBuilder App)
- {
- // Static files not compressed by Static File Middleware.
- App.UseStaticFiles();
- App.UseResponseCompression();
- App.UseMvcWithDefaultRoute();
- }
4.Use,Run 和 Map 方法
你可以使用 Use,Run 和 Map 配置 HTTP 管道.
- 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.
- ");
- });
- }
- }
下面表格使用前面的代码显示来自 https://localhost:5001 的请求和响应.
请求
响应
localhost:5001
Hello from non-Map delegate.
- localhost:5001/map1
- Map Test 1
- localhost:5001/map2
- Map Test 2
- localhost:5001/map3
Hello from non-Map delegate.
由上面可以了解到当使用 Map 方法时, 将从 HttpRequest.Path 中删除匹配的路径段, 并针对每个请求将该路径追加到 HttpRequest.PathBase.
类型的任何谓词均可用于将请求映射到管道的新分支(HandleBranch). 在以下示例中, 谓词用于检测查询字符串变量 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.
- ");
- });
- }
- }
下面表格使用前面的代码显示来自 https://localhost:5001 的请求和响应.
请求
响应
https://localhost:5001
Hello from non-Map delegate.
- https://localhost:5001/?branch=master
- Branch used = master
Map 支持嵌套, 例如:
- public void Configure(IApplicationBuilder App)
- {
- 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.");
- });
- }
- }
5. 编写中间件(重点)
虽然 ASP.NET Core 为我们提供了一组丰富的内置中间件组件, 但在某些情况下, 你可能需要写入自定义中间件.
5.1 中间件类
通常, 中间件应该封装在自定义类中, 并且通过扩展方法公开.
- 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}");
- });
- }
- }
可通过传入区域性参数测试该中间件. 例如 https://localhost:7997/?culture=zh,https://localhost:7997/?culture=en.
- // 自定义 RequestCultureMiddleware 类
- public class RequestCultureMiddleware
- {
- private readonly RequestDelegate _next;
- public RequestCultureMiddleware(RequestDelegate next)
- {
- _next = next;
- }
- public async Task InvokeAsync(HttpContext context)
- {
- context.Response.ContentType = "text/plain; charset=utf-8";
- 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);
- }
- }
5.2 中间件扩展方法
中间件扩展方法可以通过 IApplicationBuilder 公开中间件. 示例创建一个 RequestCultureMiddlewareExtensions 扩展类并通过 IApplicationBuilder 公开:
- public static class RequestCultureMiddlewareExtensions
- {
- public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware();
- }
- }
再通过 Startup.Configure 方法调用中间件:
- public class Startup
- {
- public void Configure(IApplicationBuilder App)
- {
- App.UseRequestCulture();
- App.Run(async (context) =>
- {
- await context.Response.WriteAsync(
- $"Hello {CultureInfo.CurrentCulture.DisplayName}");
- });
- }
- }
响应结果:
由此整个自定义 ASP.NET Core 中间件完成.
6. 按请求依赖项
因为中间件是在应用程序启动时构建的, 而不是每个请求时构建, 所以在每个请求期间, 中间件构造函数使用的范围内生命周期服务不与其他依赖关系注入类型共享. 如果您必须在中间件和其他类型之间共享作用域服务, 请将这些服务添加到 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);
- }
- }
- public static class CustomMiddlewareExtensions
- {
- public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware();
- }
- }
- public interface IMyScopedService
- {
- void MyProperty(decimal input);
- }
- public class MyScopedService : IMyScopedService
- {
- public void MyProperty(decimal input)
- {
- Console.WriteLine("MyProperty is" + input);
- }
- }
- public void ConfigureServices(IServiceCollection services)
- {
- // 注入 DI 服务
- services.AddScoped();
- }
响应结果:
参考文献:
来源: https://www.2cto.com/kf/201905/809468.html