(4)ASP.NET Core 中间件
(4)ASP.NET Core 中间件
1.前言
中间件(middleware)是一种装配到应用管道以处理请求和响应的组件。每个组件:
●可选择是否将请求传递到管道中的下一个组件。
●可在管道中的下一个组件前后执行工作。
请求委托(request delegates)用于建立请求管道(request pipeline),请求委托处理每个HTTP请求。
请求委托通过使用IApplicationBuilder类型的Run、Map和Use扩展方法来配置,并在Strartup类中传给Configure方法。每个单独的请求委托都可以被指定为一个内嵌匿名方法(称为并行中间件,in-line middleware),或者其定义在一个可重用的类中。这些可重用的类被称作“中间件”或“中间件组件”。请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。当中间件短路时,它被称为“终端中间件”(terminal middleware),因为它阻止中间件进一步处理请求。
2.使用 IApplicationBuilder 创建中间件管道
ASP.NET Core请求管道包含一系列请求委托,依次调用。下图演示了这一概念。沿黑色箭头执行。
每个委托(中间件)均可在下一个委托前后执行操作。任何委托都能选择停止传递到下一个委托,转而自己处理该请求,这就是请求管道的短路(下面会举例说明)。而且是一种有意义的设计,因为它可以避免不必要的工作。比如,一个授权(authorization)中间件只有通过身份验证之后才能调用下一个委托,否则它就会被短路,并返回“Not Authorized”的响应。所以应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。
现在我们来演示下用一个简单的ASP.NET Core应用程序建立单个请求委托处理每个HTTP请求(这种情况不包括实际请求管道):
public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }
响应结果:
由上面我们可以看到,运行时输出的是Run委托消息,然后我们再定义多一个请求委托看看效果,请看如下代码:
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委托的时候就已经终止了管道,这是为什么呢?
因为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方法添加中间件组件的顺序定义了在请求上调用它们的顺序,以及响应的相反顺序。此排序对于安全性、性能和功能至关重要。
以下Startup.Configure方法将为常见应用方案添加中间件组件:
●异常/错误处理(Exception/error handling)
●HTTP严格传输安全协议(HTTP Strict Transport Security Protocol)
●HTTPS重定向(HTTPS redirection)
●静态文件服务器(Static file server)
●Cookie策略实施(Cookie policy enforcement)
●身份验证(Authentication)
●会话(Session)
●MVC
请看如下代码:
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之后才会发生。
经过上面描述,大家都了解中间件顺序的重要性了吧。以下示例演示中间件的排序,其中静态文件的请求在响应压缩中间件之前由静态文件中间件进行处理。静态文件不会按照中间件的顺序进行压缩。可以压缩来自 UseMvcWithDefaultRoute的 MVC 响应。示例:
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管道。
●Use:Use方法可使管道短路(即不调用 next 请求委托)。第二节点有示例代码演示。
●Run:Run是一种约定,并且某些中间件组件可公开在管道末尾运行的Run[Middleware]方法。第二节点有示例代码演示。
●Map: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. <p>"); }); } }
下面表格使用前面的代码显示来自http://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。
MapWhen基于给定谓词的结果创建请求管道分支。Func<HttpContext, bool>类型的任何谓词均可用于将请求映射到管道的新分支(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. <p>"); }); } }
下面表格使用前面的代码显示来自http://localhost:5001的请求和响应。
请求 |
响应 |
http://localhost:5001 |
Hello from non-Map delegate. <p> |
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}"); }); } }
可通过传入区域性参数测试该中间件。例如 http://localhost:7997/?culture=zh、http://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<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}"); }); } }
响应结果:
由此整个自定义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<CustomMiddleware>(); } } 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<IMyScopedService, MyScopedService>(); }
响应结果:
参考文献:
ASP.NET Core中间件
写入自定义ASP.NET Core中间件