您现在的位置是:网站首页> 编程资料编程资料
理解ASP.NET Core 中间件(Middleware)_实用技巧_
2023-05-24
359人已围观
简介 理解ASP.NET Core 中间件(Middleware)_实用技巧_
中间件
先借用微软官方文档的一张图:

可以看到,中间件实际上是一种配置在HTTP请求管道中,用来处理请求和响应的组件。它可以:
- 决定是否将请求传递到管道中的下一个中间件
- 可以在管道中的下一个中间件处理之前和之后进行操作
此外,中间件的注册是有顺序的,书写代码时一定要注意!
中间件管道
Run
该方法为HTTP请求管道添加一个中间件,并标识该中间件为管道终点,称为终端中间件。也就是说,该中间件就是管道的末尾,在该中间件之后注册的中间件将永远都不会被执行。所以,该方法一般只会书写在Configure方法末尾。
public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }Use
通过该方法快捷的注册一个匿名的中间件
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { // 下一个中间件处理之前的操作 Console.WriteLine("Use Begin"); await next(); // 下一个中间件处理完成后的操作 Console.WriteLine("Use End"); }); } } 注意:
- 1.如果要将请求发送到管道中的下一个中间件,一定要记得调用
next.Invoke / next(),否则会导致管道短路,后续的中间件将不会被执行 - 2.在中间件中,如果已经开始给客户端发送
Response,请千万不要调用next.Invoke / next(),也不要对Response进行任何更改,否则,将抛出异常。 - 3.可以通过
context.Response.HasStarted来判断响应是否已开始。
以下都是错误的代码写法
错误1:
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Use"); await next(); }); app.Run(context => { // 由于上方的中间件已经开始 Response,此处更改 Response Header 会抛出异常 context.Response.Headers.Add("test", "test"); return Task.CompletedTask; }); } }错误2:
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Use"); // 即使没有调用 next.Invoke / next(),也不能在 Response 开始后对 Response 进行更改 context.Response.Headers.Add("test", "test"); }); } }UseWhen
通过该方法针对不同的逻辑条件创建管道分支。需要注意的是:
进入了管道分支后,如果管道分支不存在管道短路或终端中间件,则会再次返回到主管道。
当使用PathString时,路径必须以“/”开头,且允许只有一个'/'字符
支持嵌套,即UseWhen中嵌套UseWhen等
支持同时匹配多个段,如 /get/user
public class Startup { public void Configure(IApplicationBuilder app) { // /get 或 /get/xxx 都会进入该管道分支 app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app => { app.Use(async (context, next) => { Console.WriteLine("UseWhen:Use"); await next(); }); }); app.Use(async (context, next) => { Console.WriteLine("Use"); await next(); }); app.Run(async context => { Console.WriteLine("Run"); await context.Response.WriteAsync("Hello World!"); }); } }当访问 /get 时,输出如下:
UseWhen:Use
Use
Run
如果你发现输出了两遍,别慌,看看是不是浏览器发送了两次请求,分别是 /get 和 /favicon.ico
Map
- 通过该方法针对不同的请求路径创建管道分支。需要注意的是:
- 一旦进入了管道分支,则不会再回到主管道。
- 使用该方法时,会将匹配的路径从
HttpRequest.Path中删除,并将其追加到HttpRequest.PathBase中。 - 路径必须以“/”开头,且不能只有一个
'/'字符 - 支持嵌套,即Map中嵌套Map、MapWhen(接下来会讲)等
- 支持同时匹配多个段,如 /post/user
public class Startup { public void Configure(IApplicationBuilder app) { // 访问 /get 时会进入该管道分支 // 访问 /get/xxx 时会进入该管道分支 app.Map("/get", app => { app.Use(async (context, next) => { Console.WriteLine("Map get: Use"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await next(); }); app.Run(async context => { Console.WriteLine("Map get: Run"); await context.Response.WriteAsync("Hello World!"); }); }); // 访问 /post/user 时会进入该管道分支 // 访问 /post/user/xxx 时会进入该管道分支 app.Map("/post/user", app => { // 访问 /post/user/student 时会进入该管道分支 // 访问 /post/user/student/1 时会进入该管道分支 app.Map("/student", app => { app.Run(async context => { Console.WriteLine("Map /post/user/student: Run"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await context.Response.WriteAsync("Hello World!"); }); }); app.Use(async (context, next) => { Console.WriteLine("Map post/user: Use"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await next(); }); app.Run(async context => { Console.WriteLine("Map post/user: Run"); await context.Response.WriteAsync("Hello World!"); }); }); } }当你访问 /get/user 时,输出如下:
Map get: Use
Request Path: /user
Request PathBase: /get
Map get: Run
当你访问 /post/user/student/1 时,输出如下:
Map /post/user/student: Run
Request Path: /1
Request PathBase: /post/user/student
其他情况交给你自己去尝试啦!
MapWhen
与Map类似,只不过MapWhen不是基于路径,而是基于逻辑条件创建管道分支。注意事项如下:
- 一旦进入了管道分支,则不会再回到主管道。
- 当使用
PathString时,路径必须以“/”开头,且允许只有一个'/'字符 HttpRequest.Path和HttpRequest.PathBase不会像Map那样进行特别处理- 支持嵌套,即MapWhen中嵌套MapWhen、Map等
- 支持同时匹配多个段,如 /get/user
public class Startup { public void Configure(IApplicationBuilder app) { // /get 或 /get/xxx 都会进入该管道分支 app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app => { app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app => { app.Use(async (context, next) => { Console.WriteLine("MapWhen get user: Use"); await next(); }); }); app.Use(async (context, next) => { Console.WriteLine("MapWhen get: Use"); await next(); }); app.Run(async context => { Console.WriteLine("MapWhen get: Run"); await context.Response.WriteAsync("Hello World!"); }); }); } }当你访问 /get/user 时,输出如下:
MapWhen get user: Use
可以看到,即使该管道分支没有终端中间件,也不会回到主管道。
Run & Use & UseWhen & Map & Map
一下子接触了4个命名相似的、与中间件管道有关的API,不知道你有没有晕倒,没关系,我来帮大家总结一下:
Run用于注册终端中间件,Use用来注册匿名中间件,UseWhen、Map、MapWhen用于创建管道分支。UseWhen进入管道分支后,如果管道分支中不存在短路或终端中间件,则会返回到主管道。Map和MapWhen进入管道分支后,无论如何,都不会再返回到主管道。UseWhen和MapWhen基于逻辑条件来创建管道分支,而Map基于请求路径来创建管道分支,且会对HttpRequest.Path和HttpRequest.PathBase进行处理。
编写中间件并激活
上面已经提到过的Run和Use就不再赘述了。
基于约定的中间件
“约定大于配置”,先来个约法三章:
- 1.拥有公共(public)构造函数,且该构造函数至少包含一个类型为
RequestDelegate的参数 - 2.拥有名为
Invoke或InvokeAsync的公共(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参数。
一个简单的中间件如下:
public class MyMiddleware { // 用于调用管道中的下一个中间件 private readonly RequestDelegate _next; public MyMiddleware( RequestDelegate next, ITransientService transientService, ISingletonService singletonService) { _next = next; } public async Task InvokeAsync( HttpContext context, ITransientService transientService, IScopedService scopedService, ISingletonService singletonService) { // 下一个中间件处理之前的操作 Console.WriteLine("MyMiddleware Begin"); await _next(context); // 下一个中间件处理完成后的操作 Console.WriteLine("MyMiddleware End"); } }然后,你可以通过UseMiddleware方法将其添加到管道中
public class Startup { public void Configure(IApplicationBuilder app) { app.UseMiddleware(); } } 不过,一般不推荐直接使用UseMiddleware,而是将其封装到扩展方法中
public static class AppMiddlewareApplicationBuilderExtensions { public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware(); } public class Startup { public void Configure(IApplicationBuilder app) { app.UseMy(); } } 基于工厂的中间件
优势:
相关内容
- .Net Core项目中NLog整合Exceptionless实例_实用技巧_
- .NET Core对象池的应用:扩展篇_实用技巧_
- .NET Core对象池的应用:设计篇_实用技巧_
- .NET Core对象池的应用:编程篇_实用技巧_
- NAT网络地址转换详情_ASP.NET_
- .Net Framework .Net .NET Standard的概念及区别_ASP.NET_
- C# 有关Assembly.Unload详解_实用技巧_
- ASP.NET session.timeout设置案例详解_实用技巧_
- .net core api接口JWT方式认证Token_实用技巧_
- 解决ASP.NET Core中使用漏桶算法限流的问题_实用技巧_
