首先,很感谢在上篇文章 C# 管道式编程 中给我有小额捐助和点赞的朋友们,感谢大家的支持与确定。但愿个人每一次分享都能让彼此得到一些收获,固然若是我有些地方叙述的不正确或不当,还请不客气的指出。好了,下面进入正文。html
在开始以前,咱们须要明确的一个概念是,在 Web 程序中,用户的每次请求流程都是线性的,放在 ASP.NET Core 程序中,都会对应一个 请求管道(request pipeline),在这个请求管道中,咱们能够动态配置各类业务逻辑对应的 中间件(middleware),从而达到服务端能够针对不一样用户作出不一样的请求响应。在 ASP.NET Core 中,管道式编程是一个核心且基础的概念,它的不少中间件都是经过 管道式 的方式来最终配置到请求管道中的,因此理解这里面的管道式编程对咱们编写更加健壮的 DotNetCore 程序至关重要。git
在上面的论述中,咱们提到了两个很重要的概念:请求管道(request pipeline) 和 中间件(middleware)。对于它俩的关系,我我的的理解是,首先,请求管道服务于用户,其次,请求管道能够将多个相互独立的业务逻辑模块(即中间件)串联起来,而后服务于用户请求。这样作的好处是能够将业务逻辑层级化,由于在实际的业务场景中,有些业务的处理即相互独立,又依赖于其它的业务操做,各个业务模块之间的关系其实是动态不固定的。github
下面,咱们尝试着来一步步解析 ASP.NET Core 中的管道机制。web
首先,咱们来看一下官方的图例解释:编程
从上图中,咱们不难看出,当用户发出一块儿请求后,应用程序都会为其建立一个请求管道,在这个请求管道中,每个中间件都会按顺序进行处理(可能会执行,也可能不会被执行,取决于具体的业务逻辑),等最后一个中间件处理完毕后请求又会以相反的方向返回给用户最终的处理结果。api
为了验证上述咱们的理论解释,咱们开始建立一个 DotNetCore 的控制台项目,而后引用以下包:浏览器
编写以下示例代码:微信
class Program { static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } public class Startup { public void Configure(IApplicationBuilder app) { // Middleware A app.Use(async (context, next) => { Console.WriteLine("A (in)"); await next(); Console.WriteLine("A (out)"); }); // Middleware B app.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); }); // Middleware C app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); } }
上述代码段展现了一个最简单的 ASP.NET Core Web 程序,尝试 F5 运行咱们的程序,而后打开浏览器访问 http://127.0.0.1:5000 会看到浏览器显示了 Hello World from the terminal middleware 的信息。对应的控制台信息以下图所示:架构
上述示例程序成功验证了咱们理论解释中的一些设想,这说明在 Configure 函数中成功构建了一个完成的请求管道,那既然这样,咱们就能够将其修改成咱们以前使用管道的方式,示例代码以下所示:app
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { Console.WriteLine("A (int)"); await next(); Console.WriteLine("A (out)"); }).Use(async (context, next) => { Console.WriteLine("B (int)"); await next(); Console.WriteLine("B (out)"); }).Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); } }
这两个方式都能让咱们的请求管道正常运行,只是写的方式不一样。至于采用哪一种方式彻底看我的喜爱。须要注意的是,最后一个控制台中间件须要最后注册,由于它的处理是单向的,不涉及将用户请求修改后返回。
一样的,咱们也能够对咱们的管道中间件进行条件式组装(分叉路由),组装条件能够依据具体的业务场景而定,这里我以路由为条件进行组装,不一样的访问路由最终访问的中间件是不同的,示例代码以下所示:
public class Startup { public void Configure(IApplicationBuilder app) { // Middleware A app.Use(async (context, next) => { Console.WriteLine("A (in)"); await next(); Console.WriteLine("A (out)"); }); // Middleware B app.Map( new PathString("/foo"), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); })); // Middleware C app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); } }
当咱们直接访问 http://127.0.0.1:5000 时,对应的请求路由输出以下:
对应的页面会回显 Hello World from the terminal middleware
当咱们直接访问 httP://127.0.0.1:5000/foo 时,对应的请求路由输出以下:
当咱们尝试查看对应的请求页面,发现对应的页面倒是 HTTP ERROR 404 ,经过上述输出咱们能够找到缘由,是因为最后一个注册的终端路由未能成功调用,致使不能返回对应的请求结果。针对这种状况有两种解决方法。
一种是在咱们的 路由B 中直接返回请求结果,示例代码以下所示:
app.Map( new PathString("/foo"), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); await context.Response.WriteAsync("Hello World from the middleware B"); Console.WriteLine("B (out)"); }));
这种方式不太推荐,由于它极易致使业务逻辑的不一致性,违反了 单一职责原则 的思想。
另外一种解决办法是经过路由匹配的方式,示例代码以下所示:
app.UseWhen( context => context.Request.Path.StartsWithSegments(new PathString("/foo")), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); }));
经过使用 UseWhen 的方式,添加了一个业务中间件对应的业务条件,在该中间件执行完毕后会自动回归到主的请求管道中。最终对应的日志输出入下图所示:
一样的,咱们也能够自定义一个中间件,示例代码以下所示:
public class Startup { public void Configure(IApplicationBuilder app) { // app.UseMiddleware<CustomMiddleware>(); //等价于下述调用方式 app.UseCustomMiddle(); // Middleware C app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); } } public class CustomMiddleware { private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext httpContext) { Console.WriteLine("CustomMiddleware (in)"); await _next.Invoke(httpContext); Console.WriteLine("CustomMiddleware (out)"); } } public static class CustomMiddlewareExtension { public static IApplicationBuilder UseCustomMiddle(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomMiddleware>(); } }
日志输出以下图所示:
因为 ASP.NET Core 中的自定义中间件都是经过 依赖注入(DI) 的的方式来进行实例化的。因此对应的构造函数,咱们是能够注入咱们想要的数据类型,不光是 RequestDelegate
;其次,咱们自定义的中间件还须要实现一个公有的 public void Invoke(HttpContext httpContext)
或 public async Task InvokeAsync(HttpContext httpContext)
的方法,该方法内部主要处理咱们的自定义业务,并进行中间件的链接,扮演着 枢纽中心 的角色。
因为 ASP.NET Core 是彻底开源跨平台的,因此咱们能够很容易的在 Github 上找到其对应的托管仓库。最后,咱们能够看一下 ASP.NET Core 官方的一些实现代码。以下图所示:
官方开源了内置中间件的所有实现代码,这里我以 健康检查(HeathChecks)
中间件为例,来验证一下咱们上面说的自定义中间件的实现。
经过查阅源码,咱们能够看出,咱们上述自定义的中间件是符合官方的实现标准的。一样的,当咱们之后使用某个内置中间件时,若是对其具体实现感兴趣,能够经过这种方式来进行查看。
当咱们对 ASP.NET Core 的请求管道进行中间件配置的时候,有一个地方须要注意一下,就是中间件的配置必定要具体的业务逻辑顺序进行,好比用户认证配置必定要先于路由配置,结合到代码就是下述示例:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //...... app.UseAuthentication(); //...... app.UseMvc(); }
若是当咱们的中间件顺序配置不当的话,极有可能致使相应的业务出现问题。
就 ASP.NET Core 的技术架构而言,管道式编程只是其中很小很基础的一部分,整个技术框架设计与实现,用到了不少优秀的技术和架构思想。可是这些高大上的实现都是基于基础技术衍化而来的,因此,基础很重要,只有把基础打扎实了,才不会被技术浪潮所淘汰。
上述全部内容就是我我的对 ASP.NET Core 中的管道式编程的一些理解和拙见,若是有不正确或不当的地方,还请斧正。
望共勉!
日期 | 赞扬者 | 金额 | 备注 |
---|---|---|---|
2019-07-29 | O*z | 2元(微信) |
原文出处:https://www.cnblogs.com/hippieZhou/p/11205573.html