注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

中间件

先借用微软官方文档的一张图:

可以看到,中间件实际上是一种配置在HTTP请求管道中,用来处理请求和响应的组件。它可以:

  • 决定是否将请求传递到管道中的下一个中间件
  • 可以在管道中的下一个中间件处理之前和之后进行操作

此外,中间件的注册是有顺序的,书写代码时一定要注意!

中间件管道

Run

该方法为HTTP请求管道添加一个中间件,并标识该中间件为管道终点,称为终端中间件。也就是说,该中间件就是管道的末尾,在该中间件之后注册的中间件将永远都不会被执行。所以,该方法一般只会书写在Configure方法末尾。

  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.Run(async context =>
  6. {
  7. await context.Response.WriteAsync("Hello, World!");
  8. });
  9. }
  10. }

Use

通过该方法快捷的注册一个匿名的中间件

  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.Use(async (context, next) =>
  6. {
  7. // 下一个中间件处理之前的操作
  8. Console.WriteLine("Use Begin");
  9. await next();
  10. // 下一个中间件处理完成后的操作
  11. Console.WriteLine("Use End");
  12. });
  13. }
  14. }

注意

  1. 如果要将请求发送到管道中的下一个中间件,一定要记得调用next.Invoke / next(),否则会导致管道短路,后续的中间件将不会被执行
  2. 在中间件中,如果已经开始给客户端发送Response,请千万不要调用next.Invoke / next(),也不要对Response进行任何更改,否则,将抛出异常。
  3. 可以通过context.Response.HasStarted来判断响应是否已开始。

以下都是错误的代码写法

  • 错误1:
  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.Use(async (context, next) =>
  6. {
  7. await context.Response.WriteAsync("Use");
  8. await next();
  9. });
  10. app.Run(context =>
  11. {
  12. // 由于上方的中间件已经开始 Response,此处更改 Response Header 会抛出异常
  13. context.Response.Headers.Add("test", "test");
  14. return Task.CompletedTask;
  15. });
  16. }
  17. }
  • 错误2:
  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.Use(async (context, next) =>
  6. {
  7. await context.Response.WriteAsync("Use");
  8. // 即使没有调用 next.Invoke / next(),也不能在 Response 开始后对 Response 进行更改
  9. context.Response.Headers.Add("test", "test");
  10. });
  11. }
  12. }

UseWhen

通过该方法针对不同的逻辑条件创建管道分支。需要注意的是:

  • 进入了管道分支后,如果管道分支不存在管道短路或终端中间件,则会再次返回到主管道。
  • 当使用PathString时,路径必须以“/”开头,且允许只有一个'/'字符
  • 支持嵌套,即UseWhen中嵌套UseWhen等
  • 支持同时匹配多个段,如 /get/user
  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. // /get 或 /get/xxx 都会进入该管道分支
  6. app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
  7. {
  8. app.Use(async (context, next) =>
  9. {
  10. Console.WriteLine("UseWhen:Use");
  11. await next();
  12. });
  13. });
  14. app.Use(async (context, next) =>
  15. {
  16. Console.WriteLine("Use");
  17. await next();
  18. });
  19. app.Run(async context =>
  20. {
  21. Console.WriteLine("Run");
  22. await context.Response.WriteAsync("Hello World!");
  23. });
  24. }
  25. }

当访问 /get 时,输出如下:

  1. UseWhen:Use
  2. Use
  3. Run

如果你发现输出了两遍,别慌,看看是不是浏览器发送了两次请求,分别是 /get 和 /favicon.ico

Map

通过该方法针对不同的请求路径创建管道分支。需要注意的是:

  • 一旦进入了管道分支,则不会再回到主管道。
  • 使用该方法时,会将匹配的路径从HttpRequest.Path 中删除,并将其追加到HttpRequest.PathBase中。
  • 路径必须以“/”开头,且不能只有一个'/'字符
  • 支持嵌套,即Map中嵌套Map、MapWhen(接下来会讲)等
  • 支持同时匹配多个段,如 /post/user
  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. // 访问 /get 时会进入该管道分支
  6. // 访问 /get/xxx 时会进入该管道分支
  7. app.Map("/get", app =>
  8. {
  9. app.Use(async (context, next) =>
  10. {
  11. Console.WriteLine("Map get: Use");
  12. Console.WriteLine($"Request Path: {context.Request.Path}");
  13. Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
  14. await next();
  15. });
  16. app.Run(async context =>
  17. {
  18. Console.WriteLine("Map get: Run");
  19. await context.Response.WriteAsync("Hello World!");
  20. });
  21. });
  22. // 访问 /post/user 时会进入该管道分支
  23. // 访问 /post/user/xxx 时会进入该管道分支
  24. app.Map("/post/user", app =>
  25. {
  26. // 访问 /post/user/student 时会进入该管道分支
  27. // 访问 /post/user/student/1 时会进入该管道分支
  28. app.Map("/student", app =>
  29. {
  30. app.Run(async context =>
  31. {
  32. Console.WriteLine("Map /post/user/student: Run");
  33. Console.WriteLine($"Request Path: {context.Request.Path}");
  34. Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
  35. await context.Response.WriteAsync("Hello World!");
  36. });
  37. });
  38. app.Use(async (context, next) =>
  39. {
  40. Console.WriteLine("Map post/user: Use");
  41. Console.WriteLine($"Request Path: {context.Request.Path}");
  42. Console.WriteLine($"Request PathBase: {context.Request.PathBase}");
  43. await next();
  44. });
  45. app.Run(async context =>
  46. {
  47. Console.WriteLine("Map post/user: Run");
  48. await context.Response.WriteAsync("Hello World!");
  49. });
  50. });
  51. }
  52. }

当你访问 /get/user 时,输出如下:

  1. Map get: Use
  2. Request Path: /user
  3. Request PathBase: /get
  4. Map get: Run

当你访问 /post/user/student/1 时,输出如下:

  1. Map /post/user/student: Run
  2. Request Path: /1
  3. Request PathBase: /post/user/student

其他情况交给你自己去尝试啦!

MapWhen

Map类似,只不过MapWhen不是基于路径,而是基于逻辑条件创建管道分支。注意事项如下:

  • 一旦进入了管道分支,则不会再回到主管道。
  • 当使用PathString时,路径必须以“/”开头,且允许只有一个'/'字符
  • HttpRequest.PathHttpRequest.PathBase不会像Map那样进行特别处理
  • 支持嵌套,即MapWhen中嵌套MapWhen、Map等
  • 支持同时匹配多个段,如 /get/user
  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. // /get 或 /get/xxx 都会进入该管道分支
  6. app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app =>
  7. {
  8. app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app =>
  9. {
  10. app.Use(async (context, next) =>
  11. {
  12. Console.WriteLine("MapWhen get user: Use");
  13. await next();
  14. });
  15. });
  16. app.Use(async (context, next) =>
  17. {
  18. Console.WriteLine("MapWhen get: Use");
  19. await next();
  20. });
  21. app.Run(async context =>
  22. {
  23. Console.WriteLine("MapWhen get: Run");
  24. await context.Response.WriteAsync("Hello World!");
  25. });
  26. });
  27. }
  28. }

当你访问 /get/user 时,输出如下:

  1. MapWhen get user: Use

可以看到,即使该管道分支没有终端中间件,也不会回到主管道。

Run & Use & UseWhen & Map & Map

一下子接触了4个命名相似的、与中间件管道有关的API,不知道你有没有晕倒,没关系,我来帮大家总结一下:

  • Run用于注册终端中间件,Use用来注册匿名中间件,UseWhenMapMapWhen用于创建管道分支。
  • UseWhen进入管道分支后,如果管道分支中不存在短路或终端中间件,则会返回到主管道。MapMapWhen进入管道分支后,无论如何,都不会再返回到主管道。
  • UseWhenMapWhen基于逻辑条件来创建管道分支,而Map基于请求路径来创建管道分支,且会对HttpRequest.PathHttpRequest.PathBase进行处理。

编写中间件并激活

上面已经提到过的RunUse就不再赘述了。

基于约定的中间件

“约定大于配置”,先来个约法三章:

  1. 拥有公共(public)构造函数,且该构造函数至少包含一个类型为RequestDelegate的参数
  2. 拥有名为InvokeInvokeAsync的公共(public)方法,必须包含一个类型为HttpContext的方法参数,且该参数必须位于第一个参数的位置,另外该方法必须返回Task类型。
  3. 构造函数中的其他参数可以通过依赖注入(DI)填充,也可以通过UseMiddleware传参进行填充。
    • 通过DI填充时,只能接收 Transient 和 Singleton 的DI参数。这是由于中间件是在应用启动时构造的(而不是按请求构造),所以当出现 Scoped 参数时,构造函数内的DI参数生命周期与其他不共享,如果想要共享,则必须将Scoped DI参数添加到Invoke/InvokeAsync来进行使用。
    • 通过UseMiddleware传参时,构造函数内的DI参数和非DI参数顺序没有要求,传入UseMiddleware内的参数顺序也没有要求,但是我建议将非DI参数放到前面,DI参数放到后面。(这一块感觉微软做的好牛皮)
  4. Invoke/InvokeAsync的其他参数也能够通过依赖注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI参数。

一个简单的中间件如下:

  1. public class MyMiddleware
  2. {
  3. // 用于调用管道中的下一个中间件
  4. private readonly RequestDelegate _next;
  5. public MyMiddleware(
  6. RequestDelegate next,
  7. ITransientService transientService,
  8. ISingletonService singletonService)
  9. {
  10. _next = next;
  11. }
  12. public async Task InvokeAsync(
  13. HttpContext context,
  14. ITransientService transientService,
  15. IScopedService scopedService,
  16. ISingletonService singletonService)
  17. {
  18. // 下一个中间件处理之前的操作
  19. Console.WriteLine("MyMiddleware Begin");
  20. await _next(context);
  21. // 下一个中间件处理完成后的操作
  22. Console.WriteLine("MyMiddleware End");
  23. }
  24. }

然后,你可以通过UseMiddleware方法将其添加到管道中

  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.UseMiddleware<MyMiddleware>();
  6. }
  7. }

不过,一般不推荐直接使用UseMiddleware,而是将其封装到扩展方法中

  1. public static class AppMiddlewareApplicationBuilderExtensions
  2. {
  3. public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>();
  4. }
  5. public class Startup
  6. {
  7. public void Configure(IApplicationBuilder app)
  8. {
  9. app.UseMy();
  10. }
  11. }

基于工厂的中间件

优势:

  • 按照请求进行激活。这个就是说,上面基于约定的中间件实例是单例的,但是基于工厂的中间件,可以在依赖注入时设置中间件实例的生命周期。
  • 使中间件强类型化(因为其实现了接口IMiddleware

该方式的实现基于IMiddlewareFactoryIMiddleware。先来看一下接口定义:

  1. public interface IMiddlewareFactory
  2. {
  3. IMiddleware? Create(Type middlewareType);
  4. void Release(IMiddleware middleware);
  5. }
  6. public interface IMiddleware
  7. {
  8. Task InvokeAsync(HttpContext context, RequestDelegate next);
  9. }

你有没有想过当我们调用UseMiddleware时,它是如何工作的呢?事实上,UseMiddleware扩展方法会先检查中间件是否实现了IMiddleware接口。 如果实现了,则使用容器中注册的IMiddlewareFactory实例来解析该IMiddleware的实例(这下你知道为什么称为“基于工厂的中间件”了吧)。如果没实现,那么就使用基于约定的中间件逻辑来激活中间件。

注意,基于工厂的中间件,在应用的服务容器中一般注册为 Scoped 或 Transient 服务

这样的话,咱们就可以放心的将 Scoped 服务注入到中间件的构造函数中了。

接下来,咱们就来实现一个基于工厂的中间件:

  1. public class YourMiddleware : IMiddleware
  2. {
  3. public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  4. {
  5. // 下一个中间件处理之前的操作
  6. Console.WriteLine("YourMiddleware Begin");
  7. await next(context);
  8. // 下一个中间件处理完成后的操作
  9. Console.WriteLine("YourMiddleware End");
  10. }
  11. }
  12. public static class AppMiddlewareApplicationBuilderExtensions
  13. {
  14. public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
  15. }

然后,在ConfigureServices中添加中间件依赖注入

  1. public class Startup
  2. {
  3. public void ConfigureServices(IServiceCollection services)
  4. {
  5. services.AddTransient<YourMiddleware>();
  6. }
  7. }

最后,在Configure中使用中间件

  1. public class Startup
  2. {
  3. public void Configure(IApplicationBuilder app)
  4. {
  5. app.UseYour();
  6. }
  7. }

微软提供了IMiddlewareFactory的默认实现:

  1. public class MiddlewareFactory : IMiddlewareFactory
  2. {
  3. // The default middleware factory is just an IServiceProvider proxy.
  4. // This should be registered as a scoped service so that the middleware instances
  5. // don't end up being singletons.
  6. // 默认的中间件工厂仅仅是一个 IServiceProvider 的代理
  7. // 该工厂应该注册为 Scoped 服务,这样中间件实例就不会成为单例
  8. private readonly IServiceProvider _serviceProvider;
  9. public MiddlewareFactory(IServiceProvider serviceProvider)
  10. {
  11. _serviceProvider = serviceProvider;
  12. }
  13. public IMiddleware? Create(Type middlewareType)
  14. {
  15. return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
  16. }
  17. public void Release(IMiddleware middleware)
  18. {
  19. // The container owns the lifetime of the service
  20. // DI容器来管理服务的生命周期
  21. }
  22. }

可以看到,该工厂使用过DI容器来解析出服务实例的。因此,当使用基于工厂的中间件时,是无法通过UseMiddleware向中间件的构造函数传参的。

基于约定的中间件 VS 基于工厂的中间件

  • 基于约定的中间件实例都是 Singleton;而基于工厂的中间件实例可以是 Singleton、Scoped 和 Transient(当然,不建议注册为 Singleton)
  • 基于约定的中间件实例构造函数中可以通过依赖注入传参,也可以用过UseMiddleware传参;而基于工厂的中间件只能通过依赖注入传参
  • 基于约定的中间件实例可以在Invoke/InvokeAsync中添加更多的依赖注入参数;而基于工厂的中间件只能按照IMiddleware的接口定义进行实现。

理解ASP.NET Core - [02] Middleware的更多相关文章

  1. 目录-理解ASP.NET Core

    <理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...

  2. ASP.NET Core中Middleware的使用

    https://www.cnblogs.com/shenba/p/6361311.html   ASP.NET 5中Middleware的基本用法 在ASP.NET 5里面引入了OWIN的概念,大致意 ...

  3. 理解 ASP.NET Core: 处理管道

    理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...

  4. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  5. 理解ASP.NET Core - 路由(Routing)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Routing Routing(路由):更准确的应该叫做Endpoint Routing,负责 ...

  6. 理解ASP.NET Core - 日志(Logging)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 快速上手 添加日志提供程序 在文章主机(Host)中,讲到Host.CreateDefault ...

  7. 理解ASP.NET Core - 错误处理(Handle Errors)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...

  8. 在ASP.NET Core使用Middleware模拟Custom Error Page功能

    一.使用场景 在传统的ASP.NET MVC中,我们可以使用HandleErrorAttribute特性来具体指定如何处理Action抛出的异常.只要某个Action设置了HandleErrorAtt ...

  9. ASP.NET Core中间件(Middleware)实现WCF SOAP服务端解析

    ASP.NET Core中间件(Middleware)进阶学习实现SOAP 解析. 本篇将介绍实现ASP.NET Core SOAP服务端解析,而不是ASP.NET Core整个WCF host. 因 ...

随机推荐

  1. CF1032G Chattering

    CF1032G Chattering 题意 思路 对于每一个位置,它转移的范围是确定的. 对于一段可以走到的区间,我们可以求出区间中所有点再能走到区间范围. 于是这个就可以倍增进行转移. 如何快速求出 ...

  2. Android系统编程入门系列之界面Activity响应多元的属性动画

    在响应丝滑动画一篇文章中,分别介绍了作用于普通视图.绘制视图的绘制对象.和界面这三种对象的动画效果,但是都有一些使用的局限性.比如这些动画都只是以屏幕上绘制更新的方式绘制动画,并没有真实改变作用对象的 ...

  3. jvm源码解读--17 Java的wait()、notify()学习

    write and debug by 张艳涛 wait()和notify()的通常用法 A线程取得锁,执行wait(),释放锁; B线程取得锁,完成业务后执行notify(),再释放锁; B线程释放锁 ...

  4. 冒泡排序、选择排序、直接插入排序、快速排序、折半查找>从零开始学JAVA系列

    目录 冒泡排序.选择排序.直接插入排序 冒泡排序 选择排序 选择排序与冒泡排序的注意事项 小案例,使用选择排序完成对对象的排序 直接插入排序(插入排序) 快速排序(比较排序中效率最高的一种排序) 折半 ...

  5. Java 在Word中创建多级项目符号列表和编号列表

    本文分享通过Java程序代码在Word中创建多级项目符号列表和编号列表的方法.程序运行环境如下: IntelliJ IDEA 2018(JDK 1.8.0) Word 2013 Word Jar包:F ...

  6. Vulhub-DC-4靶场

    Vulhub-DC-4靶场 前言 这套靶场的亮点在于对hydra的运用比较多,在遇到大容量字典的时候,BurpSuite可能会因为设置的运行内存的限制,导致字典需要花很长时间导入进去,虽然通过修改配置 ...

  7. CVE-2021-3156 复现

    测试环境 OS: Ubuntu 18.04.5 LTS GCC: gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) Make: GNU Make 4.1 ...

  8. silky微服务简介

    代理主机 silky微服务定义了三种类型的代理主机,开发者可以根据需要选择合适的silky代理主机托管微服务应用.代理主机定义了一个Startup模块,该模块给出了使用该种类型主机所必须依赖的模块. ...

  9. Docker部署netcore web实践

    1. 新建一个netcore的项目 2. 我们到项目的生成输出目录下,创建一个Dockerfile文件 3. 编辑Dockerfile文件 备注:红线圈住的地方,就是你生成的netcore的程序名称 ...

  10. Compile Java Codes in Linux Shell instead of Ant Script

    The following is frequently used ant script, compile some java source codes with a libary path, then ...