时间:2022-07-04 14:05:55 | 栏目:.NET代码 | 点击:次
我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的代码中。而中间件就是用于组成应用程序管道来处理请求和响应的组件。管道内的每一个组件都可以选择是否将请求转交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个HTTP请求。
中间件可以认为有两个基本的职责:
请求委托通过使用IApplicationBuilder类型的Run、Map以及Use扩展方法来配置,并在Startup类中传给Configure方法。每个单独的请求委托都可以被指定为一个内嵌匿名方法,或其定义在一个可重用的类中。这些可以重用的类被称作“中间件”或“中间件组件”。每个位于请求管道内的中间件组件负责调用管道中下一个组件,或适时短路调用链。中间件是一个典型的AOP应用。
ASP.NET Core请求管道由一系列的请求委托所构成,它们一个接着一个的被调用,看下面一张微软官方的中间件请求管道图(图中执行线程按黑色箭头的顺序执行):
中间件短路:每一个委托在下一个委托之前和之后都有机会执行操作。任何委托都能选择停止传递到下一个委托,而是结束请求并开始响应,这就是请求管道的短路,这是一种有意义的设计,因为它可以避免一些不必要的工作。比如说,一个授权(authorization)中间件只有在通过身份验证之后才能调用下一个委托,否则它就会被短路,并返回“Not Authorized”的响应。异常处理委托需要在管道的早期被调用,这样它们就能够捕捉到发生在管道内更深层次出现的异常了。短路可以用下面这张图来表示:
在上图中,我们可以把中间件1认为是身份认证的中间件,HTTP请求发送过来,首先经过身份认证中间件,如果身份认证失败,那么就直接给出响应并返回,不会再把请求传递给下面的中间件2和中间件3.
中间件的执行跟调用的顺序有关,然后在响应时则以相反的顺序返回。
请求在每一步都可能被短路,所以我们要以正确的顺序添加中间件,如异常处理中间件,我们要添加在最开始的地方,这样就能第一时间捕获异常,以及后续中间可能发生的异常,然后最终做处理返回。
我们来看看Configure方法里面提供了哪些中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // 异常中间件 app.UseDeveloperExceptionPage(); } // 路由中间件 app.UseRouting(); // 授权中间件 app.UseAuthorization(); // 终结点中间件 app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
中间件和过滤器都是一种AOP的思想,他们的功能类似,那么他们有什么区别呢?
过滤器更加贴合业务,它关注于应用程序本身,关注的是如何实现业务,比如对输出结果进行格式化,对请求的ViewModel进行数据校验,这时就肯定要使用过滤器了。过滤器是MVC的一部分,它可以拦截到你Action上下文的一些信息,而中间件是没有这个能力的。可以认为过滤器是附加性的一种功能,它只是中间件附带表现出来的特征。中间件是管道模型里重要的组成部分,不可或缺,而过滤器可以没有。
中间件中定义了Run、Use、Map、MapWhen几种方法,我们下面一一讲解这几种方法。
我们先来看到Run()方法的定义:
中定义中可以看出:Run()方法中只有一个RequestDelegate委托类型的参数,没有Next参数,所以Run()方法也叫终端中间件,不会将请求传递给下一个中间件,也就是发生了“短路”。看下面的代码:
// Run方法向应用程序的请求管道中添加一个RequestDelegate委托 // 放在管道最后面,终端中间件 app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World1\r\n"); }); app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World2\r\n"); });
程序运行结果:
可以看到:只输出了中间件1的信息,没有输出中间件2的信息,说明发生了短路。
注意:Run()方法被称为终端中间件,要放在所有中间件的最后面,否则在Run()方法后面的中间件将不会被执行。
我们先来看看Use()方法的定义:
可以看出:Use方法的参数是一个Func委托,输入参数是一个RequestDelegate类型的委托,返回参数也是一个RequestDelegate类型的委托,这里表示调用下一个中间件,我们在来看看RequestDelegate委托的定义:
可以看出:RequestDelegate是一个委托,有一个HttpContext类型的参数,HttPContext表示Http请求上下文,可以获取请求信息,返回值是Task类型,明白了Use()方法的参数以后,我们写一个自定义的Use()方法:
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。 // context参数是HttpContext,表示HTTP请求的上下文对象 // next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路 // 用Use可以将多个中间件链接在一起 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use1\r\n"); // 调用下一个委托 await next(); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use2\r\n"); // 调用下一个委托 await next(); });
程序运行结果:
我们在上面说过,可以在调用中间件之前和之后做一些工作,看下面的代码:
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。 // context参数是HttpContext,表示HTTP请求的上下文对象 // next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路 // 用Use可以将多个中间件链接在一起 app.Use(async (context, next) => { // 解决中文乱码问题 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中间件1:传入请求\r\n"); // 调用下一个委托 await next(); await context.Response.WriteAsync(text: "中间件1:传出响应\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中间件2:传入请求\r\n"); // 调用下一个委托 await next(); await context.Response.WriteAsync(text: "中间件2:传出响应\r\n"); }); app.Run(handler:async context => { await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n"); });
程序运行结果:
我们可以总结上面代码的执行顺序:
我们知道:Use()方法中有两个参数,next参数表示调用管道中的下一个中间件,如果不调用next,那么也会使管道发生短路,相当于Run()方法,看下面的代码:
// 向应用程序的请求管道中添加一个Func委托,这个委托其实就是所谓的中间件。 // context参数是HttpContext,表示HTTP请求的上下文对象 // next参数表示管道中的下一个中间件委托,如果不调用next,则会使管道短路 // 用Use可以将多个中间件链接在一起 app.Use(async (context, next) => { // 解决中文乱码问题 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中间件1:传入请求\r\n"); // 调用下一个委托 await next(); await context.Response.WriteAsync(text: "中间件1:传出响应\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中间件2:传入请求\r\n"); // 调用下一个委托 await next(); await context.Response.WriteAsync(text: "中间件2:传出响应\r\n"); }); //app.Run(handler:async context => //{ // await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n"); //}); // Use方法也可以不调用next,表示发生短路 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中间件3:处理请求并生成响应\r\n"); });
程序运行结果:
可以看出:如果使用Use()方法,不调用next,实现的效果跟使用Run()方法一样,都会使管道发生短路。
Map作为惯例,将管道分流。Map根据给定请求路径匹配将请求管道分流。如果请求路径以指定路径开始,则执行分支。看一下Map()方法的定义:
可以看到Map方法有两个参数:第一个参数是匹配规则,第二个参数是Action泛型委托,泛型委托参数是IApplicationBuilder类型,和Configure方法的第一个参数类型相同。这就表示可以把实现了Action泛型委托的方法添加到中间件管道中执行。
我们首先定义一个方法,该方法的参数是IApplicationBuilder类型:
/// <summary> /// 自定义方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap1(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map1"); }); } /// <summary> /// 自定义方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap2(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map2"); }); }
然后看一下使用Map方法的代码:
// Map可以根据匹配的URL来选择执行,简单来说就是根据URL进行分支选择执行 // 有点类似于MVC中的路由 // 匹配的URL:http://localhost:5000/Map1 app.Map(pathMatch: "/Map1", configuration: HandleMap1); // 匹配的URL:http://localhost:5000/Map2 app.Map(pathMatch: "/Map2", configuration: HandleMap2);
运行程序,然后在浏览器地址栏里面输入:http://localhost:5000/Map1,输出结果:
在地址栏里面在输入:http://localhost:5000/Map2,输出结果:
Map还支持嵌套,看下面的代码:
// 嵌套Map app.Map(pathMatch: "/Map1", configuration: App1 => { // App1.Map("/Map2",action=> { action.Run(async context => { await context.Response.WriteAsync("This is /Map1/Map2"); }); }); App1.Run(async context => { await context.Response.WriteAsync("This is no-map"); }); });
访问http://localhost:5000/Map1/123输出结果:
访问http://localhost:5000/Map1输出结果:
访问http://localhost:5000/Map1/Map2输出结果:
Map也可以同时匹配多个段,看下面的代码:
运行程序,输出结果:
访问http://localhost:5000/Map1/Map2输出结果:
MapWhen是基于给定的谓词分支请求管道。任何使Func<HttpContext,bool>返回true的谓词的请求都被映射到新的管道分支。
我们先来看看Mapwhen方法的定义:
可以看出:MapWhen方法有两个参数:第一个参数是Func类型的委托,输入参数是HttpContext,输出参数是bool类型。第二个参数是Action委托,参数是IApplicationBuilder类型,表示也可以把实现Action委托的方法添加到中间件管道中执行。
看下面的例子,如果url中包括name查询参数,则执行HandleName方法,如果包含age查询参数,则执行HandleAge方法,否则执行Run()方法。
HandleName和HandleAge方法定义如下:
private void HandleName(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This name is: {context.Request.Query["name"]}"); }); } private void HandleAge(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This age is: {context.Request.Query["age"]}"); }); }
对应的MapWhen方法定义如下:
// 如果访问的url参数中包含name,则执行HandleName app.MapWhen( // Func委托,输入参数是HttpContext,返回bool值 predicate: context => { // 判断url参数中是否包含name return context.Request.Query.ContainsKey("name"); }, configuration: HandleName); // 如果访问的url参数中包含name,则执行HandleAge app.MapWhen( // Func委托,输入参数是HttpContext,返回bool值 predicate: context => { // 判断url参数中是否包含age return context.Request.Query.ContainsKey("age"); }, configuration: HandleAge); app.Run(async context => { await context.Response.WriteAsync("There is non-Map delegate \r\n"); });
运行程序,输出结果:
在url里面添加name查询参数输出结果:
在url里面添加age查询参数输出结果:
在上面的例子中,我们都是使用的官方中间件自动的方法,其实我们也可以自己编写一个中间件。
中间件遵循显示依赖原则,并在其构造函数中暴露所有依赖项。中间件能够利用UseMiddleware<T>扩展方法的优势,直接通过它们的构造函数注入服务。依赖注入服务是自动完成填充的。
ASP.NET Core约定中间件类必须包括以下内容:
我们自定义一个记录IP的中间件,新建一个类RequestIPMiddleware,代码如下:
using Microsoft.AspNetCore.Http; using System.Threading.Tasks; namespace MiddlewareDemo.Middleware { /// <summary> /// 记录IP地址的中间件 /// </summary> public class RequestIPMiddleware { // 私有字段 private readonly RequestDelegate _next; /// <summary> /// 公共构造函数,参数是RequestDelegate类型 /// 通过构造函数进行注入,依赖注入服务会自动完成注入 /// </summary> /// <param name="next"></param> public RequestIPMiddleware(RequestDelegate next) { _next = next; } /// <summary> /// Invoke方法 /// 返回值是Task,参数类型是HttpContext /// </summary> /// <param name="context">Http上下文</param> /// <returns></returns> public async Task Invoke(HttpContext context) { await context.Response.WriteAsync($"User IP:{context.Connection.RemoteIpAddress.ToString()}\r\n"); // 调用管道中的下一个委托 await _next.Invoke(context); } } }
然后创建一个扩展方法,对IApplicationBuilder进行扩展:
using Microsoft.AspNetCore.Builder; namespace MiddlewareDemo.Middleware { public static class RequestIPExtensions { /// <summary> /// 扩展方法,对IApplicationBuilder进行扩展 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder) { // UseMiddleware<T> return builder.UseMiddleware<RequestIPMiddleware>(); } } }
最后在Startup类的Configure方法中使用自定义中间件:
// 使用自定义中间件 app.UseRequestIP();
运行程序,查看结果:
这样就完成了一个自定义中间件。
当应用程序在开发环境中运行时,开发人员异常页中间件( UseDeveloperExceptionPage )报告应用程序运行时的错误。
当应用程序在生产环境中运行时,异常处理中间件( UseExceptionHandler )捕获下面中间件中引发的异常。
HTTPS重定向中间件( UseHttpsRedirection )会将HTTP请求重定向到HTTPS。
静态文件中间件( UseStaticFiles )返回静态文件,并简化进一步请求处理。
Cookie策略中间件( UseCookiePolicy )使应用符合欧盟一般数据保护条例的规定。
路由中间件( UseRouting )用于路由的请求。
身份认证中间件( UseAuthentication )尝试对用户进行身份验证,验证通过之后才会允许用户访问安全资源。
授权中间件( UseAuthorization )用于授权验证通过的用户可以访问哪些资源。
会话中间件( UseSession )建立和维护会话状态。如果应用程序使用会话状态,请在Cookie策略中间件之后和MVC中间件之前调用会话中间件。
终结点路由中间件( UseEndpoints )用于将 Razor Pages 终结点添加到请求管道。
更多中间件组件可以到aspnet 的GitHub仓库中查看:https://github.com/aspnet。
示例代码GitHub地址:https://github.com/jxl1024/Middleware