本文通过一张GIF动图来继续聊一下ASP.NET Core的请求处理管道,从管道的配置、构建以及请求处理流程等方面做一下详细的研究。(ASP.NET Core系列目录

一、概述

  上文说到,请求是经过 Server监听=>处理成httpContext=>Application处理生成Response。 这个Application的类型RequestDelegate本质是 public delegate Task RequestDelegate (HttpContext context); ,即接收HttpContext并返回Task, 它是由一个个中间件 Func<RequestDelegate, RequestDelegate> middleware 嵌套在一起构成的。它的构建是由ApplicationBuilder完成的,先来看一下这个ApplicationBuilder:

  1. public class ApplicationBuilder : IApplicationBuilder
  2. {
  3. private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
  4. public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
  5. {
  6. _components.Add(middleware);
  7. return this;
  8. }
  9. public RequestDelegate Build()
  10. {
  11. RequestDelegate app = context =>
  12. {
  13. context.Response.StatusCode = ;
  14. return Task.CompletedTask;
  15. };
  16. foreach (var component in _components.Reverse()) {
  17. app = component(app);
  18. }
  19. return app;
  20. }
  21. }

  ApplicationBuilder有个集合 IList<Func<RequestDelegate, RequestDelegate>> _components 和一个用于向这个集合中添加内容的  Use(Func<RequestDelegate, RequestDelegate> middleware) 方法,通过它们的类型可以看出来它们是用来添加和存储中间件的。现在说一下大概的流程:

  1. 调用startupFilters和_startup的Configure方法,调用其中定义的多个UseXXX(进一步调用ApplicationBuilder的Use方法)将一个个中间件middleware按照顺序写入上文的集合_components(记住这个_components)。
  2. 定义了一个 context.Response.StatusCode =  的RequestDelegate。
  3. 将集合_components颠倒一下, 然后遍历其中的middleware,一个个的与新创建的404 RequestDelegate 连接在一起,组成一个新的RequestDelegate(即Application)返回。

  这个最终返回的RequestDelegate类型的Application就是对HttpContext处理的管道了,这个管道是多个中间件按照一定顺序连接在一起组成的,startupFilters先不说,以我们非常熟悉的Startup为例,它的Configure方法默认情况下已经依次进行了UseBrowserLink、UseDeveloperExceptionPage、UseStaticFiles、UseMvc了等方法,请求进入管道后,请求也会按照这个顺序来经过各个中间件处理,首先进入UseBrowserLink,然后UseBrowserLink会调用下一个中间件UseDeveloperExceptionPage,依次类推到达UseMVC后被处理生成Response开始逆向返回再依次反向经过这几个中间件,正常情况下,请求到达MVC中间件后被处理生成Response开始逆向返回,而不会到达最终的404,这个404是为了防止其他层未配置或未能处理的时候的一个保险操作。  

  胡扯两句:这个管道就像一座塔,话说唐僧路过金光寺去扫金光塔,从前门进入第一层开始扫,然后从前门的楼梯进入第二层、第三层、第四层,然后从第四层的后门扫下来直至后门出去,却不想妖怪没处理好, 被唐僧扫到了第五层(顶层)去,发现佛宝被奔波儿灞和霸波尔奔偷走了,大喊:悟空悟空,佛宝被妖怪偷走啦!(404...)

  下面就以这4个为例通过一个动图形象的描述一下整个过程:

图1

  一个“中规中矩”的管道就是这样构建并运行的,通过上图可以看到各个中间件在Startup文件中的配置顺序与最终构成的管道中的顺序的关系,下面我们自己创建几个中间件体验一下,然后再看一下不“中规中矩”的长了杈子的管道。

二、自定义中间件

  先仿照系统现有的写一个

  1. public class FloorOneMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. public FloorOneMiddleware(RequestDelegate next)
  5. {
  6. _next = next;
  7. }
  8. public async Task InvokeAsync(HttpContext context)
  9. {
  10. Console.WriteLine("FloorOneMiddleware In");
  11. //Do Something
  12. //To FloorTwoMiddleware
  13. await _next(context);
  14. //Do Something
  15. Console.WriteLine("FloorOneMiddleware Out");
  16. }
  17. }

  这是塔的第一层,进入第一层后的 //Do Something 表示在第一层需要做的工作, 然后通过 _next(context) 进入第二层,再下面的 //Do Something 是从第二层出来后的操作。同样第二层调用第三层也是一样。再仿写个UseFloorOne的扩展方法:

  1. public static class FloorOneMiddlewareExtensions
  2. {
  3. public static IApplicationBuilder UseFloorOne(this IApplicationBuilder builder)
  4. {
  5. Console.WriteLine("Use FloorOneMiddleware");
  6. return builder.UseMiddleware<FloorOneMiddleware>();
  7. }
  8. }

这样在Startup的Configure方法中就也可以写 app.UseFloorOne(); 将这个中间件作为管道的一部分了。

  通过上面的例子仿照系统默认的中间件完成了一个简单的中间件的编写,这里也可以用简要的写法,直接在Startup的Configure方法中这样写:

  1. app.Use(async (context,next) =>
  2. {
  3. Console.WriteLine("FloorThreeMiddleware In");
  4. //Do Something
  5. //To FloorThreeMiddleware
  6. await next.Invoke();
  7. //Do Something
  8. Console.WriteLine("FloorThreeMiddleware Out");
  9. });

同样可以实现上一种例子的工作,但还是建议按照那样的写法,在Startup这里体现的简洁并且可读性好的多。

复制一下第一种和第二种的例子,形成如下代码:

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. app.UseFloorOne();
  4. app.UseFloorTwo();
  5. app.Use(async (context,next) =>
  6. {
  7. Console.WriteLine("FloorThreeMiddleware In");
  8. //Do Something
  9. //To FloorThreeMiddleware
  10. await next.Invoke();
  11. //Do Something
  12. Console.WriteLine("FloorThreeMiddleware Out");
  13. });
  14. app.Use(async (context, next) =>
  15. {
  16. Console.WriteLine("FloorFourMiddleware In");
  17. //Do Something
  18. await next.Invoke();
  19. //Do Something
  20. Console.WriteLine("FloorFourMiddleware Out");
  21. });
  22.  
  23. if (env.IsDevelopment())
  24. {
  25. app.UseBrowserLink();
  26. app.UseDeveloperExceptionPage();
  27. }
  28. else
  29. {
  30. app.UseExceptionHandler("/Home/Error");
  31. }
  32.  
  33. app.UseStaticFiles();
  34.  
  35. app.UseMvc(routes =>
  36. {
  37. routes.MapRoute(
  38. name: "default",
  39. template: "{controller=Home}/{action=Index}/{id?}");
  40. });
  41. }

运行一下看日志:

  1. CoreMiddleware> Use FloorOneMiddleware
  2. CoreMiddleware> Use FloorTwoMiddleware
  3. CoreMiddleware> Hosting environment: Development
  4. CoreMiddleware> Content root path: C:\Users\FlyLolo\Desktop\CoreMiddleware\CoreMiddleware
  5. CoreMiddleware> Now listening on: http://localhost:10757
  6. CoreMiddleware> Application started. Press Ctrl+C to shut down.
  7. CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[]
  8. CoreMiddleware> Request starting HTTP/1.1 GET http://localhost:56440/
  9. CoreMiddleware> FloorOneMiddleware In
  10. CoreMiddleware> FloorTwoMiddleware In
  11. CoreMiddleware> FloorThreeMiddleware In
  12. CoreMiddleware> FloorFourMiddleware In
  13. CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[]
  14. CoreMiddleware> Executing action method CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) with arguments ((null)) - ModelState is Valid
  15. CoreMiddleware> info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[]
  16. CoreMiddleware> Executing ViewResult, running view at path /Views/Home/Index.cshtml.
  17. CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[]
  18. CoreMiddleware> Executed action CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) in .6822ms
  19. CoreMiddleware> FloorFourMiddleware Out
  20. CoreMiddleware> FloorThreeMiddleware Out
  21. CoreMiddleware> FloorTwoMiddleware Out
  22. CoreMiddleware> FloorOneMiddleware Out
  23. CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[]
  24. CoreMiddleware> Request finished in .8944ms text/html; charset=utf-

可以看到,前两行的Use FloorOneMiddleware和Use FloorTwoMiddleware是将对应的中间件写入集合_components,而中间件本身并未执行,然后10至12行是依次经过我们自定义的例子的处理,第13-18就是在中间件MVC中的处理了,找到并调用对应的Controller和View,然后才是19-22的逆向返回, 最终Request finished返回状态200, 这个例子再次验证了请求在管道中的处理流程。

那么我们试一下404的情况, 把Configure方法中除了自定义的4个中间件外全部注释掉,再次运行

  1. //上面没变化 省略
  2. CoreMiddleware> FloorOneMiddleware In
  3. CoreMiddleware> FloorTwoMiddleware In
  4. CoreMiddleware> FloorThreeMiddleware In
  5. CoreMiddleware> FloorFourMiddleware In
  6. CoreMiddleware> FloorFourMiddleware Out
  7. CoreMiddleware> FloorThreeMiddleware Out
  8. CoreMiddleware> FloorTwoMiddleware Out
  9. CoreMiddleware> FloorOneMiddleware Out
  10. CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[]
  11. CoreMiddleware> Request finished in .7216ms

可以看到,MVC处理的部分没有了,因为该中间件已被注释,而最后一条可以看到系统返回了状态404。

那么既然MVC可以正常处理请求没有进入404, 我们怎么做可以这样呢?是不是不调用下一个中间件就可以了? 试着把FloorFour改一下

  1. app.Use(async (context, next) =>
  2. {
  3. Console.WriteLine("FloorFourMiddleware In");
  4. //await next.Invoke();
  5. await context.Response.WriteAsync("Danger!");
  6. Console.WriteLine("FloorFourMiddleware Out");
  7. });

再次运行,查看输出和上文的没有啥太大改变, 只是最后的404变为了200, 网页上的“404 找不到。。”也变成了我们要求输出的"Danger!", 达到了我们想要的效果。

但一般情况下我们不这样写,ASP.NET Core 提供了Use、Run和Map三种方法来配置管道。

三、Use、Run和Map

  Use上面已经用过就不说了,对于上面的问题, 一般用Run来处理,Run主要用来做为管道的末尾,例如上面的可以改成这样:

  1. app.Run(async (context) =>
  2. {
  3. await context.Response.WriteAsync("Danger!");
  4. });

  因为本身他就是作为管道末尾,也就省略了next参数,虽然用use也可以实现, 但还是建议用Run。

   Map:

   static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration); pathMatch用于匹配请求的path, 例如“/Home”, 必须以“/”开头, 判断path是否是以pathMatch开头。

若是,则进入 Action<IApplicationBuilder> configuration) , 这个参数是不是长得很像startup的Configure方法? 这就像进入了我们配置的另一个管道,它是一个分支,如下图

图2

做个例子:

  1. app.UseFloorOne();
  2. app.Map("/Manager", builder =>
  3. {
  4. builder.Use(async (context, next) =>
  5. {
  6. await next.Invoke();
  7. });
  8.  
  9. builder.Run(async (context) =>
  10. {
  11. await context.Response.WriteAsync("Manager.");
  12. });
  13. });
  14. app.UseFloorTwo();

  进入第一层后, 添加了一个Map, 作用是当我们请求 localhost:/Manager/index 这样的地址的时候(是不是有点像Area), 会进入这个Map创建的新分支, 结果也就是页面显示"Manager." 不会再进入下面的FloorTwo。若不是“/Manager”开头的, 这继续进入FloorTwo。虽然感觉这个Map灵活了我们的管道配置, 但这个只能匹配path开头的方法太局限了,不着急, 我们看一下MapWhen。

  Map When:

  MapWhen方法就是一个灵活版的Map,它将原来的PathMatch替换为一个 Func<HttpContext, bool> predicate ,这下就开放多了,它返回一个bool值,现在举个栗子随便改一下

  1. app.MapWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder =>
  2. {
  3. //...TODO...
  4. }

  当根据请求的参数是否包含“XX”的时候进入这个分支。

  从图2可知,一旦进入分支,是无法回到原分支的, 如果只是想在某种情况下进入某些中间件,但执行完后还可以继续后续的中间件怎么办呢?对比MapWhen,Use也有个UseWhen。

  UseWhen:

   它和MapWhen一样,当满足条件的时候进入一个分支,在这个分支完成之后再继续后续的中间件,当然前提是这个分支中没有Run等短路行为。

  1. app.UseWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder =>
  2. {
  3. //...TODO...
  4. }

四、IStartupFilter

  我们只能指定一个Startup类作为启动类,那么还能在其他的地方定义管道么? 文章开始的时候说到,构建管道的时候,会调用startupFilters和_startup的Configure方法,调用其中定义的多个UseXXX方法来将中间件写入_components。自定义一个StartupFilter,实现IStartupFilter的Configure方法,用法和Startup的Configure类似,不过要记得最后调用 next(app) 。

  1. public class TestStartupFilter : IStartupFilter
  2. {
  3. public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
  4. {
  5. return app => {
  6. app.Use(async (context, next1) =>
  7. {
  8. Console.WriteLine("filter.Use1.begin");
  9. await next1.Invoke();
  10. Console.WriteLine("filter.Use1.end");
  11. });
  12. next(app);
  13. };
  14. }
  15. }

在复制一个,去startup的ConfigureServices注册一下:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMvc();
  4. services.AddSingleton<IStartupFilter,TestStartupFilter>();
  5. services.AddSingleton<IStartupFilter, TestStartupFilter2>();
  6. }

这样的配置就生效了,现在剖析一下他的生效机制。回顾一下WebHost的BuildApplication方法:

  1. private RequestDelegate BuildApplication()
  2. {
  3. //....省略
  4. var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
  5. Action<IApplicationBuilder> configure = _startup.Configure;
  6. foreach (var filter in startupFilters.Reverse())
  7. {
  8. configure = filter.Configure(configure);
  9. }
  10.  
  11. configure(builder);
  12.  
  13. return builder.Build();
  14. }

  仔细看这段代码,其实这和构建管道的流程非常相似,对比着说一下:

  1. 首先通过GetService获取到注册的IStartupFilter集合startupFilters(类比_components)
  2. 然后获取Startup的Configure(类比404的RequestDelegate)
  3. 翻转startupFilters,foreach它并且与Startup的Configure链接在一起。
  4. 上文强调要记得最后调用 next(app),这个是不是和 next.Invoke() 类似。

  是不是感觉和图一的翻转拼接过程非常类似,是不是想到了拼接先后顺序的问题。对比着管道构建后中间件的执行顺序,体会一下后,这时应该可以想到各个IStartupFilter和Startup的Configure的执行顺序了吧。没错就是按照依赖注入的顺序:TestStartupFilter=>TestStartupFilter2=>Startup。

ASP.NET Core 2.0 : 八.图说管道,唐僧扫塔的故事的更多相关文章

  1. ASP.NET Core 2.0 : 八.图说管道

    本文通过一张GIF动图来继续聊一下ASP.NET Core的请求处理管道,从管道的配置.构建以及请求处理流程等方面做一下详细的研究.(ASP.NET Core系列目录) 一.概述 上文说到,请求是经过 ...

  2. ASP.NET Core 1.0中的管道-中间件模式

    ASP.NET Core 1.0借鉴了Katana项目的管道设计(Pipeline).日志记录.用户认证.MVC等模块都以中间件(Middleware)的方式注册在管道中.显而易见这样的设计非常松耦合 ...

  3. ASP.NET Core 2.0 支付宝当面付之扫码支付

    前言 自从微软更换了CEO以后,微软的战略方向有了相当大的变化,不再是那么封闭,开源了许多东西,拥抱开源社区,.NET实现跨平台,收购xamarin并免费提供给开发者等等.我本人是很喜欢.net的,并 ...

  4. Core 1.0中的管道-中间件模式

    ASP.NET Core 1.0中的管道-中间件模式 SP.NET Core 1.0借鉴了Katana项目的管道设计(Pipeline).日志记录.用户认证.MVC等模块都以中间件(Middlewar ...

  5. ASP.NET Core 3.0 : 二十八. 在Docker中的部署以及docker-compose的使用

    本文简要说一下ASP.NET Core 在Docker中部署以及docker-compose的使用  (ASP.NET Core 系列目录). 系统环境为CentOS 8 . 打个广告,求职中.. 一 ...

  6. ASP.NET Core 3.0 入门

    原文:ASP.NET Core 3.0 入门 课程简介 与2.x相比发生的一些变化,项目结构.Blazor.SignalR.gRPC等 课程预计结构 ASP.NET Core 3.0项目架构简介 AS ...

  7. [译]Writing Custom Middleware in ASP.NET Core 1.0

    原文: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/ Middleware是ASP. ...

  8. ASP.NET Core 1.0 静态文件、路由、自定义中间件、身份验证简介

    概述 ASP.NET Core 1.0是ASP.NET的一个重要的重新设计. 例如,在ASP.NET Core中,使用Middleware编写请求管道. ASP.NET Core中间件对HttpCon ...

  9. 从头编写 asp.net core 2.0 web api 基础框架 (1)

    工具: 1.Visual Studio 2017 V15.3.5+ 2.Postman (Chrome的App) 3.Chrome (最好是) 关于.net core或者.net core 2.0的相 ...

随机推荐

  1. sqlzoo:4

    列出每個國家的名字 name,當中人口 population 是高於俄羅斯'Russia'的人口. SELECT name FROM world WHERE population > (SELE ...

  2. 使用anaconda创建tensorflow环境后如何在jupyter notebook中使用

    在以下目录中 C:\Users\UserName\AppData\Roaming\jupyter\kernels\python3 打开kernel.json文件,将python.exe文件的路径修改至 ...

  3. string 转 int

    1.stringstream 用流转换 cin    cout都是流的操作   iostream cin的时候,从屏幕读取字符串流,自动判断类型(省去了scanf的格式控制) stringstream ...

  4. DataTable的Merge\COPY\AcceptChange使用说明

    在C#内使用DataTable的Merge().Copy().AcceptChange().Clone()方法的用途如下: 1.Merge()可将两个不同的表结构的表进行合并,合并后新表的列为之前两表 ...

  5. jenkins安装配置

    一.下载Jenkins 官网地址:https://jenkins.io/,图如下所示,点击下载可下载最新版本. 点击下载之后,我们可以看到下面的图,我这边选择的Jenkins.war 文件. 下面,使 ...

  6. js 原型链的介绍

    对象的原型链:一个对象所拥有的属性不仅仅是它本身拥有的属性,他还会从其他对象中继承一些属性.当js在一个对象中找不到需要的属性时,它会到这个对象的父对象上去找,以此类催,这就构成了对象的原型链. 下面 ...

  7. Spring源码工程导入Eclsipse缺少两个jar文件

    按照<Spring源码深度解析>所述,使用gradle cleanidea eclipse将Spring源码转为eclipse工程后,导入eclipse,最后发现还是缺少spring-cg ...

  8. SQL 查询当前时间

    Mysql: select date_format(now(),'%Y-%m-%d'); Oracle: Oracle中如何获取系统当前时间进行语句的筛选是SQL语句的常见功能 获取系统当前时间dat ...

  9. Echarts自定义tootips

    由于业务需求,现在要自定义tootips; 设计稿如下: 代码如下: app.title = '坐标轴刻度与标签对齐'; var str1 = "top:-20px;border:0px s ...

  10. Go语言数组和切片的原理

    目录 数组 创建 访问和赋值 切片 结构 初始化 访问 追加 拷贝 总结 数组和切片是 Go 语言中常见的数据结构,很多刚刚使用 Go 的开发者往往会混淆这两个概念,数组作为最常见的集合在编程语言中是 ...