ASP.NET Core 中间件
我们知道在 ASP.NET 中,有一个面向切面的请求管道,由22个主要的事件构成,能够让我们在往预定的执行顺序里面添加自己的处理逻辑。一般采取两种方式:一种是直接在 Global.asax 中对应的方法中直接添加代码。一种是是在 web.config 中通过注册 HttpModule 来实现对请求管道事件监听,并通过 HttpHandler 进入到我们的应用程序中。而在 ASP.NET Core 中,对请求管道进行了重新设计,通过使用一种称为中间件的方式来进行管道的注册,同时也变得更加简洁和强大。关于 HTTP请求流程和管道模型可以看这篇博客 https://www.cnblogs.com/xuhuale/p/10030878.html
什么是中间件 ?
中间件 (Middleware) 是组装到应用程序管道中以处理请求和响应的组件。 管道内的每个组件都可以选择是否将请求交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个HTTP请求。
请求委托通过使用 IApplicationBuilder 类型的 Run、Map 以及 Use 扩展方法来配置,并在 Startup 类中传给 Configure方法。每个单独的清求委托都可以被指定为一个内嵌匿名方法,或其定义在一个可重用的类中。这些可重用的类被称作 “中间件”或“中间件组件”。每个位于请求管道内的中间件组件负责调用管道中的下一个组件,或适时短路调用链。
ASP.NET请求管道由一系列的请求委托所构成,它们一个接着一个被调用,如图所示(该执行线程按黑色箭头的顺序执行)。
每个委托:
- 均可在下一个委托前后执行操作;
- 委托还可以决定是否将请求传递给下一个委托(任何委托都能选择停止传递到下一个委托,转而自己处理该请求,这就是请求管道的短路)。
短路是一种有意义的设计,因为它可以避免不必要的工作。比如说:
- 静态文件中间件可以返回静态文件请求并使管道的其余部分短路。
- 授权中间件只有通过身份验证之后才能调用下一个中间件,否则就会被他短路。
先在管道中调用异常处理委托,以便它们可以捕获在管道的后期阶段所发生的异常。
IApplicationBuilder
在笔记一中,我们就简单介绍过 IApplicationBuilder,在 Startup 类的 Configure方法中,第一个参数便是 IApplicationBuilder。
首先,IApplicationBuilder 是用来构建请求管道的,而所谓请求管道,本质上就是对 HttpContext 的一系列操作,即通过对 Request 的处理,来生成 Reponse。在 ASP.NET Core 中定义了一个 RequestDelegate 委托,来表示请求管道中的一个步骤,而对请求管道的注册是通过 Func<RequestDelegate, RequestDelegate> 类型的委托(也就是中间件)来实现的。
使用 IApplicationBuilder 创建中间件
可以先看一下 VisualStudio2017 中默认Web(MVC)站点模板关于请求管道设置的例子。
在 Startup.cs 的 Configure 方法默认增加了下列这些中间件组件:
- 异常/错误处理(同时针对开发环境和非开发环境)
- Https重定向
- 静态文件服务器
- Cookie 策略实施
- MVC
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// 开发环境
app.UseDeveloperExceptionPage();
}
else
{
// 非开发环境
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();// Https重定向
app.UseStaticFiles(); // 静态文件
app.UseCookiePolicy(); // 使用Cookie策略
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}); // MVC
}
上面的代码在非开发环境中,UseExceptionHandler 是第一个被加入到管道中的中间件,因此将会捕获之后调用组件中出现的任何异常,然后跳转到设置的异常页(/Home/Error)。
使用 UseHttpsRedirection 中间件,可以轻松实施HTTPS,将HTTP重定向到HTTPS(ASP.NET Core 2.1具有新功能)。
然后就是静态文件中间件 UseStaticFiles,静态文件中间件不提供授权检查,由它提供的任何文件,包括那些位于wwwroot下的文件都是公开可被访问的。UseCookiePolicy 是使用ASP.NET Core中的Cookie策略(详解Microsoft.AspNetCore.CookiePolicy),管道的最后执行的也就是MVC框架。
顺序
向 Startup.Configure 方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此排序对于安全性、性能和功能至关重要。请求在每一步都可能被短路,所以我们要以正确的顺序添加中间件,如异常处理,我们需要添加在最开始,这样我们就能第一时间捕获异常,以及后续中间可能发生的异常,然后最终做处理返回。
最简单的ASP.NET应用程序(空白Web模板)是使用单个请求委托来处理所有请求。事实上,在这种情况下,并不存在所谓的“管道”,针对每个HTTP请求都调用单个匿名函数来处理。
public void Configure(IApplicationBuilder app)
{
// 使用单个请求委托来处理所有请求
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
上方代码中 app.Run() 会中断管道,调用单个匿名函数来处理HTTP请求。在下面的例子中,Run了两个委托,只有第一个委托(“Hello,World!”)会被运行。
public void Configure(IApplicationBuilder app)
{
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome!");
});
}
通过运行项目,发现确实只有在第一个委托执行了,app.Run 终止了管道。
你可以将多个请求委托 app.Use 连接在一起,next参数表示管道内的下一个请求委托。在管道中,可以通过不调用next参数来终止或短路管道。通常可以在执行下一个委托之前和之后执行一些操作,如下例所示:
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Handing Request.\r\n");
// 调用管道中的下一个委托
await next.Invoke();
await context.Response.WriteAsync("Finish Handing Request.\r\n");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello from Middleware\r\n");
});
}
运行项目,在浏览器中访问出现如下结果:
Run、Map 与 Use 方法
你可以使用 Run、Map 和 Use 方法配置 HTTP 管道。
Run 方法会短路管道,因为它不会调用 next 请求委托。因此 Run 方法一般只在管道底部被调用。Run 方法是一种惯例约定,有些中间件组件可能会暴露它们自己的 Run[Middleware]方法,而这些方法只能在管道末尾处运行。
Use 前面已经简单介绍通过 Use 构建请求管道的例子,Use 方法亦可使管道短路(即不调用 next 请求委托)。
Map 扩展方法用匹配基于请求路径的请求委托,Map只接受路径,并配置单独的中间件管道的功能。 Map* 方法可以基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。如下面例子中:
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 1 </p>");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 2 </p>");
});
}
任何基于路径 /map1 的请求都会被管道中所配置 HandleMapTest1 方法处理。基于路径 /map2 的请求都会被管道中所配置 HandleMapTest2 方法处理。
下表显示上例来自 http://localhost:52831 的请求和响应。
请求 | 响应 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/map1 | Map Test 1 |
http://localhost:52831/map2 | Map Test 2 |
http://localhost:52831/map3 | Hello from non-Map delegate. |
除了基于路径的映射外,MapWhen 方法还支持基于谓语的中间件分支,允许以非常灵活的方式构建单独的管道。任何 Func<HttpContext, bool> 类型的谓语都可以被用于将请求映射到新的管道分支。下例中使用了一个简单的谓语来检测查洵字符串中变量 branch 是否存在:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
}
使用了上面的设置后,任何包含请求字符 branch 的请求将使用定义于 HandleBranch 方法内的管道,如果没有包含查询字符串 branch 的请求,将被 app.Run 所定义的委托处理。
请求 | 响应 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/?branch=1 | Branch used = 1 |
http://localhost:52831/?branch=2 | Branch used = 2 |
http://localhost:52831/?branch | Branch used = |
另外还可以嵌套 Maps:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});
Map 也可以一次匹配多个片段,例如:
app.Map("/level1/level2", HandleMultiSeg); // "/level1/level2"
以下 Startup.Configure 方法将为常见应用方案添加中间件组件:
中间件 | 描述 |
---|---|
身份验证(Authentication) | 提供身份验证支持 |
跨域资源共享(CORS) | 配置跨域资源共享 |
响应支持(Response Caching) | 提供缓存响应支持 |
响应压缩(Response Compression) | 提供响应压缩支持 |
路由(Routing) | 定义和约束请求路由 |
会话(Session) | 提供对管理用户会话(session)的支持 |
静态文件(Static Files) | 为静态文件和目录浏览提供服务提供支持 |
URL Rewriting Middleware | 用于重写 Url,并将请求重定向的支持 |
更多中间件组件可以到 Microsoft 文档 上查看。
实战中间件
如何实现一个中间件呢,下面我们来实际操作。
中间件遵循 显式依赖原则 ,并在其构造函数中暴露所有的依赖项。中间件能够利用 UseMiddleware<T> 扩展方法的优势,直接通过它们的构造函数注入服务。依赖注入服务是自动完成填充的,扩展所用到的 params 参数数组被用于非注入参数。
下面来实现一个记录IP的中间件。
① 新建一个 ASP.NET Core WebApplication 项目,选择空的模板。
然后为项目添加—个 Microsoft.Extensions.Logging.Console。
NuGet命令行执行(请使用官方源):
Install-Package Microsoft.Extensions.Logging.Console
② 新建一个类 RequestIPMiddleware.es,将中间件委托移动到类:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public class RequestIPMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestIPMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger<RequestIPMiddleware>();
}
public async Task Invoke(HttpContext context)
{
_logger.LogInformation("User IP:" + context.Connection.RemoteIpAddress.ToString());
await _next.Invoke(context);
}
}
}
③ 再新建—个 RequestIPExtensions.cs,以下扩展方法通过 IApplicationBuilder 公开中间件:
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public static class RequestIPExtensions
{
public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestIPMiddleware>();
}
}
}
这样就编写好了一个中间件。
④ 使用中间件。在 Startup.cs 中添加 app.UseRequestIP() 来使用中间件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseRequestIP();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
然后运行程序,这里选择使用 Kestrel
访问:http://localhost:5000/
成功运行,到这里我们还可以对这个中间件进行进一步改进,增加更多的功能,如限制访问等。
参考原文
Microsoft 文档 ASP.NET Core 中间件
ASP.NET Core 中间件(Middleware)详解
《ASP.NET Core 跨平台开发从入门到实战》