理解ASP.NET Core - [02] Middleware
注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录
中间件
先借用微软官方文档的一张图:

可以看到,中间件实际上是一种配置在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");
});
}
}
注意:
- 如果要将请求发送到管道中的下一个中间件,一定要记得调用
next.Invoke / next(),否则会导致管道短路,后续的中间件将不会被执行 - 在中间件中,如果已经开始给客户端发送
Response,请千万不要调用next.Invoke / next(),也不要对Response进行任何更改,否则,将抛出异常。 - 可以通过
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就不再赘述了。
基于约定的中间件
“约定大于配置”,先来个约法三章:
- 拥有公共(public)构造函数,且该构造函数至少包含一个类型为
RequestDelegate的参数 - 拥有名为
Invoke或InvokeAsync的公共(public)方法,必须包含一个类型为HttpContext的方法参数,且该参数必须位于第一个参数的位置,另外该方法必须返回Task类型。 - 构造函数中的其他参数可以通过依赖注入(DI)填充,也可以通过
UseMiddleware传参进行填充。- 通过DI填充时,只能接收 Transient 和 Singleton 的DI参数。这是由于中间件是在应用启动时构造的(而不是按请求构造),所以当出现 Scoped 参数时,构造函数内的DI参数生命周期与其他不共享,如果想要共享,则必须将Scoped DI参数添加到
Invoke/InvokeAsync来进行使用。 - 通过
UseMiddleware传参时,构造函数内的DI参数和非DI参数顺序没有要求,传入UseMiddleware内的参数顺序也没有要求,但是我建议将非DI参数放到前面,DI参数放到后面。(这一块感觉微软做的好牛皮)
- 通过DI填充时,只能接收 Transient 和 Singleton 的DI参数。这是由于中间件是在应用启动时构造的(而不是按请求构造),所以当出现 Scoped 参数时,构造函数内的DI参数生命周期与其他不共享,如果想要共享,则必须将Scoped DI参数添加到
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<MyMiddleware>();
}
}
不过,一般不推荐直接使用UseMiddleware,而是将其封装到扩展方法中
public static class AppMiddlewareApplicationBuilderExtensions
{
public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>();
}
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseMy();
}
}
基于工厂的中间件
优势:
- 按照请求进行激活。这个就是说,上面基于约定的中间件实例是单例的,但是基于工厂的中间件,可以在依赖注入时设置中间件实例的生命周期。
- 使中间件强类型化(因为其实现了接口
IMiddleware)
该方式的实现基于IMiddlewareFactory和IMiddleware。先来看一下接口定义:
public interface IMiddlewareFactory
{
IMiddleware? Create(Type middlewareType);
void Release(IMiddleware middleware);
}
public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}
你有没有想过当我们调用UseMiddleware时,它是如何工作的呢?事实上,UseMiddleware扩展方法会先检查中间件是否实现了IMiddleware接口。 如果实现了,则使用容器中注册的IMiddlewareFactory实例来解析该IMiddleware的实例(这下你知道为什么称为“基于工厂的中间件”了吧)。如果没实现,那么就使用基于约定的中间件逻辑来激活中间件。
注意,基于工厂的中间件,在应用的服务容器中一般注册为 Scoped 或 Transient 服务。
这样的话,咱们就可以放心的将 Scoped 服务注入到中间件的构造函数中了。
接下来,咱们就来实现一个基于工厂的中间件:
public class YourMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 下一个中间件处理之前的操作
Console.WriteLine("YourMiddleware Begin");
await next(context);
// 下一个中间件处理完成后的操作
Console.WriteLine("YourMiddleware End");
}
}
public static class AppMiddlewareApplicationBuilderExtensions
{
public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>();
}
然后,在ConfigureServices中添加中间件依赖注入
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<YourMiddleware>();
}
}
最后,在Configure中使用中间件
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseYour();
}
}
微软提供了IMiddlewareFactory的默认实现:
public class MiddlewareFactory : IMiddlewareFactory
{
// The default middleware factory is just an IServiceProvider proxy.
// This should be registered as a scoped service so that the middleware instances
// don't end up being singletons.
// 默认的中间件工厂仅仅是一个 IServiceProvider 的代理
// 该工厂应该注册为 Scoped 服务,这样中间件实例就不会成为单例
private readonly IServiceProvider _serviceProvider;
public MiddlewareFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMiddleware? Create(Type middlewareType)
{
return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
}
public void Release(IMiddleware middleware)
{
// The container owns the lifetime of the service
// DI容器来管理服务的生命周期
}
}
可以看到,该工厂使用过DI容器来解析出服务实例的。因此,当使用基于工厂的中间件时,是无法通过UseMiddleware向中间件的构造函数传参的。
基于约定的中间件 VS 基于工厂的中间件
- 基于约定的中间件实例都是 Singleton;而基于工厂的中间件实例可以是 Singleton、Scoped 和 Transient(当然,不建议注册为 Singleton)
- 基于约定的中间件实例构造函数中可以通过依赖注入传参,也可以用过
UseMiddleware传参;而基于工厂的中间件只能通过依赖注入传参 - 基于约定的中间件实例可以在
Invoke/InvokeAsync中添加更多的依赖注入参数;而基于工厂的中间件只能按照IMiddleware的接口定义进行实现。
理解ASP.NET Core - [02] Middleware的更多相关文章
- 目录-理解ASP.NET Core
<理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...
- ASP.NET Core中Middleware的使用
https://www.cnblogs.com/shenba/p/6361311.html ASP.NET 5中Middleware的基本用法 在ASP.NET 5里面引入了OWIN的概念,大致意 ...
- 理解 ASP.NET Core: 处理管道
理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...
- 理解ASP.NET Core - [01] Startup
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...
- 理解ASP.NET Core - 路由(Routing)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Routing Routing(路由):更准确的应该叫做Endpoint Routing,负责 ...
- 理解ASP.NET Core - 日志(Logging)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 快速上手 添加日志提供程序 在文章主机(Host)中,讲到Host.CreateDefault ...
- 理解ASP.NET Core - 错误处理(Handle Errors)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...
- 在ASP.NET Core使用Middleware模拟Custom Error Page功能
一.使用场景 在传统的ASP.NET MVC中,我们可以使用HandleErrorAttribute特性来具体指定如何处理Action抛出的异常.只要某个Action设置了HandleErrorAtt ...
- ASP.NET Core中间件(Middleware)实现WCF SOAP服务端解析
ASP.NET Core中间件(Middleware)进阶学习实现SOAP 解析. 本篇将介绍实现ASP.NET Core SOAP服务端解析,而不是ASP.NET Core整个WCF host. 因 ...
随机推荐
- IDEA web项目小坑
1.明明依赖包都加进来了,为什么运行起来报java.lang.ClassNotFoundException? 依赖包的路径只能为{project}web/WEB-INF/lib,如果将lib改为lib ...
- 【LeetCode】204.计数质数
问题描述: 统计所有小于非负整数 n 的质数的数量. 示例: 输入: 10 输出: 4 解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 . 这是一道简单题,但是却并没有那么直 ...
- vue.js 贡献指南(翻译)
Vue.js Contributing Guide vue 2.x 嗨! 我很高兴你有兴趣为Vue.js做贡献. 在提交您的贡献之前,请务必花点时间阅读以下指南. 行为守则 问题报告指南 PR指南 开 ...
- java小游戏java九宫格
问题来源于吾爱破解https://www.52pojie.cn/thread-1484202-1-1.html 编程目标一:根据下面的"游戏说明",实现该游戏程序,完成响应用户的输 ...
- Hadoop 3.1.1 - 概述 - 单节点安装
Hadoop: 单节点安装 目标 本文描述了如何安装和配置单机的 Hadoop,这样你可以使用 Hadoop MapReduce 和 Hadoop 分布式文件系统(HDFS)快速地尝试简单的操作. 前 ...
- 【数据结构与算法】字符串匹配(Rabin-Karp 算法和KMP 算法)
Rabin-Karp 算法 概念 用于在 一个字符串 中查找 另外一个字符串 出现的位置. 与暴力法不同,基本原理就是比较字符串的 哈希码 ( HashCode ) , 快速的确定子字符串是否等于被查 ...
- 漏洞分析:CVE 2021-3156
漏洞分析:CVE 2021-3156 漏洞简述 漏洞名称:sudo堆溢出本地提权 漏洞编号:CVE-2021-3156 漏洞类型:堆溢出 漏洞影响:本地提权 利用难度:较高 基础权限:需要普通用户权限 ...
- 仅用CSS实现图片渲染特效 (有学习到了)
前言 实现图片高亮效果等特效,就不得不提到CSS3的滤镜filter属性,CSS过滤器是一个强大的工具,可以使用它来实现不同的视觉效果(有点像浏览器的Photoshop过滤器).CSS filter属 ...
- HCIA-物理层
OSI七层模型 应-->表-->会-->传-->网-->数-->物理层 TCP/IP四层模型 两个主导协议 -->工业标准 应-->传-->网-- ...
- WPF基础:Dispatcher介绍
Disaptcher作用 不管是WinForm应用程序还是WPF应用程序,实际上都是一个进程,一个进程可以包含多个线程,其中有一个是主线程,其余的是子线程.在WPF或WinForm应用程序中,主线程负 ...