一、前景提要
在上文我们已经入门了委托:.NET进阶------深入理解委托(1)委托入门一、什么是委托 委托就相当于是一个可以存放方法的箱子,我们可以通过这个"箱子" - 掘金,现在利用委托,我们来玩点高级的东西。
1.1 什么是中间件
学过ASP .NET Core的同学应该对中间件有所了解,这里再简单的介绍一下:
中间件是一个处理链条(Pipeline),请求从上一个中间件流到下一个,直到核心业务;响应再从核心业务流回各个中间件,最终返回给用户。就像快递流程:商家 → 快递员 → 驿站 → 你(请求方向);你退货 → 驿站 → 快递员 → 商家(响应方向)。
用ASP .NET Core举例子,一个请求会按「注册顺序」依次经过每个中间件的前置逻辑 ;当核心业务(路由中间件)处理完请求后,响应会按「注册顺序的反向」依次经过每个中间件的后置逻辑。假设我发起了一个请求,各个阶段的路线分别是这样的:
请求:异常中间件 → 日志中间件 → 跨域中间件 → 认证中间件 → 路由中间件->核心业务(顺序)
响应:核心业务→ 路由中间件 → 认证中间件→ 跨域中间件 → 日志中间件->异常中间件(逆序)
| 阶段 | 执行顺序 & 具体逻辑 |
|---|---|
| 请求阶段(顺行) | 1. 异常中间件前置逻辑 → 无操作(仅准备捕获异常)2. 日志中间件前置逻辑 → 记录 "谁在什么时候发了什么请求"3. 跨域中间件前置逻辑 → 检查跨域请求,设置预检响应头4. 认证中间件前置逻辑 → 检查登录状态(已登录,放行)5. 路由中间件前置逻辑 → 转发请求到 "查询用户数据" 核心接口(执行核心业务) |
| 核心业务 | 路由中间件执行核心逻辑:查询用户数据,生成响应内容(无异常) |
| 响应阶段(逆行) | 1. 认证中间件后置逻辑 → 无操作(若有需要可补充响应头)2. 跨域中间件后置逻辑 → 补充跨域响应头(如 Access-Control-Allow-Origin)3. 日志中间件后置逻辑 → 记录 "请求处理完成,响应状态码 / 耗时"4. 异常中间件后置逻辑 → 无操作(无异常)5. 响应最终返回给前端 |
简单说就是请求顺行,响应逆行。
为什么中间件会有这样请求顺行,响应逆行神奇的功能呢?我们接下来利用委托手搓一个咱们自己的中间件框架,看完你就明白为什么中间件的执行顺序是这样的。
二、手搓中间件框架
2.1 框架准备
首先我准备了一个类,名为MethodInfo,内部有一个核心方法Show,它接收一个CustomContext对象,返回一个CustomContext对象。
cs
public class MethodInfo
{
public CustomContext Show(CustomContext context)
{
Console.WriteLine("这里执行了一个方法的核心内容!!!");
return new CustomContext();
}
}
public class CustomContext
{
}
为什么 Show 方法要接收 CustomContext 这样一个上下文对象呢?中间件框架的核心设计思想:用一个「统一的上下文对象」贯穿整个请求 / 响应链条,实现数据共享、状态传递、操作统一 ------ 这和 ASP.NET Core 的 HttpContext是完全一致的设计逻辑。
假设 Show 方法是中间件管道中的一个环节,若不用 CustomContext,而是传零散参数(比如 token、requestPath、statusCode),会出现这些致命问题:
- 参数爆炸 :中间件需要的信息越多(请求头、用户信息、响应状态、临时数据),方法参数就越多(比如
Show(string token, string path, int code, ...)),维护成本极高; - 状态无法传递 :前一个中间件(比如认证中间件)验证的 "用户是否登录" 状态,无法高效传递给后一个中间件(比如你的
Show方法),只能通过返回值层层传递,链条长了会乱成一团; - 无法统一修改 :后一个中间件修改的 "响应内容",无法让前一个中间件(比如日志中间件)感知到,比如
Show方法返回了错误信息,日志中间件无法知道; - 管道无法统一调度 :中间件框架需要统一调用所有中间件(比如 ASP.NET Core 的
RequestDelegate委托),若每个中间件的参数都不一样,框架无法设计统一的调用逻辑。
-
真实框架(ASP.NET Core) :
HttpContext包含:Request:URL、请求头、Query 参数、请求体、客户端 IP 等;Response:响应状态码、响应头、响应体、Cookie 等;User:认证后的用户信息(角色、权限);Items:中间件间临时共享的键值对(比如 "请求开始时间""限流计数");Connection:TCP 连接信息(端口、协议)。所有中间件都通过HttpContext访问这些数据,不用传一堆零散参数。
-
CustomContext(扩展后) :
CustomContext虽然现在是空的,但实际场景中会扩展出核心字段:cspublic class CustomContext { // 请求相关 public string RequestPath { get; set; } // 比如 /user public string Token { get; set; } // 认证Token // 响应相关 public int StatusCode { get; set; } = 200; // 响应状态码 public string ResponseContent { get; set; } // 响应内容 // 共享状态 public bool IsAuthenticated { get; set; } // 是否登录 public DateTime RequestStartTime { get; set; } // 请求开始时间 }为了简化理解中间件的框架,我们这里就暂且先用空的
CustomContext上下文对象。
2.2 初期框架
思考一个问题,如果此时,我要在这个核心方法执行前加一个校验登录的方法,并且再执行后再添加一个校验登录的方法,还不可以修改核心代码,我们该怎么办?
这时候就要用到我们的委托了,别忘了委托的什么,委托就是一个存储方法的盒子,我们再这个盒子内部,像夹心饼干一样的,创建两个方法,把我们的核心代码包裹起来,一个放在核心代码之前,一个放在核心代码之后,这样就可以实现不修改核心代码,并且可以添加功能的效果了~
cs
MethodInfo invokeInfo = new MethodInfo();
CustomContext context = new CustomContext();
Func func = new Func(invokeInfo.Show);
写到这一步又发现了问题:
func委托虽然可以利用+=操作往后添加其他的方法,但是无法在核心函数之前添加,只能往后追加- 追加的函数一定要跟核心函数一样,接收
CustomContext对象,返回CustomContext对象
如果不符合上述的情况,就无法随便往后追加,所以直接用+=多播委托的方式显然行不通,那怎么办?
2.3 高级委托:委托的委托
不妨换一个思路,如果我把这个原来的func委托修改了呢?
既然不让我往后追加,我自己把func委托拿到,然后在这个func委托前加个函数,在func委托后再加一个函数,不就行了吗?
问题是我们要如何拿到这个func委托,有没有一种方法,能把现在的func委托传进去,然后再把新的加工过的func委托返回来的方法呢?
这时候就要用到我们的高级委托了
cs
// 创建核心方法的实例
MethodInfo invokeInfo = new MethodInfo();
// 创建上下文
CustomContext context = new CustomContext();
// 创建初级委托,接收上下文对象,返回上下文对象
Func func = new Func(invokeInfo.Show);
// 定义中间件1:接收"下一个环节的委托",返回"包装后的委托"
Func, Func> middleWare1 =
new Func, Func>(next =>
{
// 中间件的核心:返回一个"包装后的委托"(包含前置+后置逻辑)
return new Func(context =>
{
// 1. 前置方法:请求进入中间件时执行(对应 ASP.NET Core await next() 之前)
Console.WriteLine("执行中间件1的前置方法");
// 2. 调用"下一个环节"(核心业务/下一个中间件),并传递上下文
CustomContext resultContext = next.Invoke(context);
// 3. 后置方法:下一个环节执行完成后执行(对应 ASP.NET Core await next() 之后)
Console.WriteLine("我是中间件1的后置方法");
// 4. 返回处理后的上下文
return resultContext;
});
});
这里我创建了一个middleWare1的高级委托,它接收一个Func的低级委托,返回一个Func的低级委托,这个低级委托其实就是我们上面说的func委托,现在我们就可以把func委托传进去,然后让middleWare给我们生成一个新的委托了。
如何把func委托传进去,生成一个新的委托呢?
cs
next =>
{
// 中间件的核心:返回一个"包装后的委托"(包含前置+后置逻辑)
return new Func(context =>
{
// 1. 前置方法:请求进入中间件时执行(对应 ASP.NET Core await next() 之前)
Console.WriteLine("执行中 间件1的前置方法");
// 2. 调用"下一个环节"(核心业务/下一个中间件),并传递上下文
CustomContext resultContext = next.Invoke(context);
// 3. 后置方法:下一个环节执行完成后执行(对应 ASP.NET Core await next() 之后)
Console.WriteLine("我是中间件1的后置方法");
// 4. 返回处理后的上下文
return resultContext;
});
}
注意看我这里用了一个Lamade表达式,它将接收的委托next变成一个新的式子委托:return new Func(context =>这个新的委托可以从代码看出:
cs
new Func(context =>
{
// 1. 前置方法:请求进入中间件时执行(对应 ASP.NET Core await next() 之前)
Console.WriteLine("执行中 间件1的前置方法");
// 2. 调用"下一个环节"(核心业务/下一个中间件),并传递上下文
CustomContext resultContext = next.Invoke(context);
// 3. 后置方法:下一个环节执行完成后执行(对应 ASP.NET Core await next() 之后)
Console.WriteLine("我是中间件1的后置方法");
// 4. 返回处理后的上下文
return resultContext;
});
这个委托的方法其实就是在原来的基础上,给原来的委托套上前置方法和后置方法。我们来看一下如何调用。
cs
// 用中间件1包装核心业务委托 → 得到"包含中间件逻辑+核心业务"的新委托
var func1 = middleWare1.Invoke(func);
// 执行包装后的委托(触发完整的中间件流程)
var customContext = func1.Invoke(context);
当执行 func1.Invoke(context);后,context上下文会通过前置函数->核心函数->后置函数执行。
我们再添加一个中间件时,情况会有所变化。
cs
MethodInfo invokeInfo = new MethodInfo();
CustomContext context = new CustomContext();
Func func = new Func(invokeInfo.Show);
Func, Func> middleWare1 =
new Func, Func>(next =>
{
return new Func(context =>
{
Console.WriteLine("执行中间件1的前置方法");
CustomContext resultContext = next.Invoke(context);
Console.WriteLine("我是中间件1的后置方法");
return resultContext;
});
});
Func, Func> middleWare2 =
new Func, Func>(next =>
{
return new Func(context =>
{
Console.WriteLine("执行中间件2的前置方法");
CustomContext resultContext = next.Invoke(context);
Console.WriteLine("我是中间件2的后置方法");
return resultContext;
});
});
Func, Func> middleWare3 =
new Func, Func>(next =>
{
return new Func(context =>
{
Console.WriteLine("执行中间件3的前置方法");
CustomContext resultContext = next.Invoke(context);
Console.WriteLine("我是中间件3的后置方法");
return resultContext;
});
});
var func1 = middleWare1.Invoke(func);
var func2 = middleWare2.Invoke(func1);
var func3 = middleWare3.Invoke(func2);
var customContext = func3.Invoke(context);
此时我们可以看到,原来的核心代码,被middlewa1中间件包裹后,又被middlewa2中间件包裹,又被middlewa3中间件包裹,当我们执行func3.Invoke(context)时,执行顺序是:
- 执行委托3:
func3.Invoke(context) - 执行委托3的前置方法:
Console.WriteLine("执行中间件3的前置方法"); - 执行
next.Invoke(context);进入委托2 - 执行委托2的前置方法:
Console.WriteLine("执行中间件2的前置方法"); - 执行
next.Invoke(context);进入委托1 - 执行委托1的前置方法:
Console.WriteLine("执行中间件1的前置方法"); - 执行核心方法
- 执行委托1的后置方法:
Console.WriteLine("执行中间件1的后置方法"); - 退回到委托2,执行委托2后置方法:
Console.WriteLine("执行中间件2的后置方法"); - 退回到委托3,执行委托3后置方法:
Console.WriteLine("执行中间件3的后置方法"); - 退出委托3
以上就是函数执行的全部过程,结果为:

跟我们一开始说的中间件的允许流程请求顺行,响应逆行保持一致,至此我们就顺利的写出了我们自己的中间件框架,虽然不够完善,但是有利于让大家理解中间件。后续我会封装一套完整的中间件框架,敬请期待。