ASP.NET Core - 中间件与管道(1)
今天来讨论一个ASP.NET Core 很重要概念管道和中间件,在ASP.NET Core中,针对HTTP请求采用pipeline也就是通常说的管道方式来处理,而管道容器内可以挂载很多中间件(处理逻辑)“串联”来处理HTTP请求,每一个中间件都有权决定是否需要执行下一个中间件,或者直接做出响应。这样的机制使得HTTP请求能够很好的被层层处理和控制,并且层次清晰处理起来甚是方便。 示意图如下:
为了再次说明管道和中间件的概念,举一个官方给出的权限验证的例子,中间件A,B分别按顺序挂载在管道容器中,A为权限验证中间件,只有通过A的权限验证才能执行B,如果没有通过A的验证,A有权中断管道处理直接返回相应的错误提示例如401等。这样必须由上一节点来调用的串行递归执行方式就是pipeline,而每一个节点就是中间件或者叫中间组件。现在我们来看看如何在ASP.NET Core中使用中间件和管理自己的HTTP管道
环境配置与Startup
在了解中间件之前我们需要先知道Startup这个类具体运作方式,我们以下面这段代码为例:
/// <summary> /// web宿主的入口类 /// </summary> public class Startup { //加入服务项到容器, 这个方法将会被runtime调用 public void ConfigureServices(IServiceCollection services) { } /// <summary> /// 配置HTTP请求管道 /// </summary> /// <param name="app">被用于构建应用程序的请求管道 只可以在Startup中的Configure方法里使用</param> /// <param name="env">提供了访问应用程序属性,如环境变量</param> /// <param name="loggerFactory">提供了创建日志的机制</param> public void Configure(IApplicationBuilder app,IHostingEnvironment env,ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) //根据配置的环境为开发环境,则会配置抛出异常错误界面 { app.UseDeveloperExceptionPage(); //抛出详细的异常错误界面 } //管道断路 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
可以看到 Startup.cs 内有两个方法,一个是用来配置接口服务到管道容器中的ConfigureServices, 一个是用来配置管道中间件的Configure。
为什么必须是这两个方法名?
其实这两个方法名并不是规定死的,但也不是任意规定的,他是根据容器的环境变量来判断的,这里先给出官方文档《多环境下工作》。
我们可以在文档中了解到,Core使用“ASPNETCORE_ENVIRONMENT”字段来描述当前运行环境名称这就是上文中提到的环境配置,官方预设了3个环境名分别是Development(开发环境), Staging(测试环境), Production(生产环境),如果您使用的是VSCode您可以在.vscode文件夹下的launch.json中找到“ASPNETCORE_ENVIRONMENT”字段,可以发现默认情况下是Development,那说这些到底有什么用呢?
在Startup中规定,配置服务和中间件两个方法可以根据环境名称来命名和选择调用,命名规则为ConfigureServices{ENVIRONMENT}和Configure{ENVIRONMENT}。如 ASPNETCORE_ENVIRONMENT = “Development” 则ConfigureServices和Configure 可以写成ConfigureServicesDevelopment 和 ConfigureDevelopment ,其他也是如此。这样就可以通过配置ASPNETCORE_ENVIRONMENT 来决定该调用哪一个配置方法了。
ConfigureServices和Configure是什么环境下的呢?
ConfigureServices和Configure就好像Switch 语句中的 default一样的道理,如果没有找到任何符合环境名的方法名,就会执行调用这两个方法。如配置了Development,但却没有给出ConfigureServicesDevelopment ,这时就会执行ConfigureServices,如果都没有就会抛出异常。
必须设置成预设环境名吗?
环境名配置的参数名不必是预设值,你可以自己写一个,比如LogEnv等等。
接下来我们看一下实现的代码:
/// <summary> /// web宿主的入口类 /// </summary> public class Startup { //加入服务项到容器, 这个方法将会被runtime调用 public void ConfigureServices(IServiceCollection services) { } /// <summary> /// Log环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureLogHelp(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureLogHelp"); }); } /// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment"); }); } /// <summary> /// 默认情况下配置HTTP请求管道 /// </summary> /// <param name="app">被用于构建应用程序的请求管道。只可以在 Startup 中的 Configure 方法里使用</param> /// <param name="env">提供了访问应用程序属性,如环境变量</param> /// <param name="loggerFactory">提供了创建日志的机制</param> public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //管道断路 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }
当ASPNETCORE_ENVIRONMENT = “Development”
当ASPNETCORE_ENVIRONMENT = “LogHelp”
这样做的好处就是你可以写自己的测试配置而不会影响到其他人或者开发过程。当然环境的作用还在于前端应该引用什么样的CSS和JS,关于这些我们之后在MVC的章节再来讨论, 想了解的博友可以看官方文档
管道配置与Startup
说完环境配置和Startup的关系,我们回来接着聊管道的事情,现在我们来说说Configure{ENVIRONMENT}一下Configure简称这个方法。
Configure这个方法是用于配置中间件到中管道容器(IApplicationBuilder),所以这个方法必须要包含一个IApplicationBuilder参数用来接受管道容器,方便开发者配置。当然他还可以接受其他的可选参数供开发者使用如下:
(注:下图来源于ASP.NET Core中文文档)
需要提一下的是,刚刚我们上文中说的环境名在IHostingEnvironment中可以获取,对于预设值官方还做了判断封装,当然你可以重构它来封装自己的环境名判断。
HTTP管道容器由三个扩展的方法来控制中间件的路由、挂载等等,分别是Run, Map, User。
a. Run方法会使得可以使管道短路,顾名思义就是终结管道向下执行不会调用next()委托,所以Run方法最好放在管道的最后来执行,如下面的代码:
/// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment"); }); app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment 不会被执行"); }); }
执行结果:
b. Use不会主动短路整个HTTP管道,但是也不会主动调用下一个中间件,必须自行调用await next.Invoke(); 如果不使用这个方法去调用下一个中间件那么Use此时的效果其实和Run是相同的,我们来看正常的代码:
/// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ var order =""; app.Use(async (context, next) => { order = $"{order}|Use start"; await next.Invoke(); order = $"{order}|Use end"; }); app.Run(async context => { await context.Response.WriteAsync($"{order}|Run ext"); }); }
执行结果如下:
可以看到,Use end并没有被执行到,因为在调用下一个中间件时采用了Run,管道被终止了。
再来看看如果不显式调用next.Invoke()时的代码:
/// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ var order =""; app.Use(async (context, next) => { order = $"{order}|Use start"; //去掉显示调用下一个中间件 //await next.Invoke(); order = $"{order}|Use end"; await context.Response.WriteAsync(order); }); app.Run(async context => { await context.Response.WriteAsync($"{order}|Run ext"); }); }
其结果如下:
可以发现Run这个中间件并没有被执行,而只是单纯的执行了Use这个中间件。所以说 在不显式调用下一个中间件的情况下,效果和Run时一样的会使管道短路。
c. Map可以根据提供的URL来路由中间件,如下代码判断URL中访问"/test"时就会执行某个中间件逻辑:
/// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Map("/test", HandleMapTest); } /// <summary> /// maptest 处理方法 /// </summary> public void HandleMapTest(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("HandleMapTest Handler"); }); }
结果如下:
如果访问/test就会执行相应的中间件,反之则不会执行。
MapWhen是Map的一个条件判断的扩展方法,可以通过它来判断某个条件适合的时候执行某一个中间件,如:当携带某一个参数名称时,执行某一个中间件或者反之,代码如下:
/// <summary> /// 开发环境下配置HTTP请求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.MapWhen(context => { return context.Request.Query.ContainsKey("username"); }, HandleUserName); app.Run(async context => { await context.Response.WriteAsync("default ext"); }); } /// <summary> /// /// </summary> public void HandleUserName(IApplicationBuilder app){ app.Run(async context => { await context.Response.WriteAsync("UserName Map"); }); }
结果如下:
Map还可以进行嵌套路由中间件,这里不再描述,大家可以参看这里。
今天的管道介绍就写到这里,希望能对大家有帮助,如有不对望多加指正。