.NET进阶——深入理解委托(2)嵌套委托:手写中间件框架

一、前景提要

在上文我们已经入门了委托:.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,而是传零散参数(比如 tokenrequestPathstatusCode),会出现这些致命问题:

  1. 参数爆炸 :中间件需要的信息越多(请求头、用户信息、响应状态、临时数据),方法参数就越多(比如 Show(string token, string path, int code, ...)),维护成本极高;
  2. 状态无法传递 :前一个中间件(比如认证中间件)验证的 "用户是否登录" 状态,无法高效传递给后一个中间件(比如你的 Show 方法),只能通过返回值层层传递,链条长了会乱成一团;
  3. 无法统一修改 :后一个中间件修改的 "响应内容",无法让前一个中间件(比如日志中间件)感知到,比如 Show 方法返回了错误信息,日志中间件无法知道;
  4. 管道无法统一调度 :中间件框架需要统一调用所有中间件(比如 ASP.NET Core 的 RequestDelegate 委托),若每个中间件的参数都不一样,框架无法设计统一的调用逻辑。
  • 真实框架(ASP.NET Core)HttpContext 包含:

    • Request:URL、请求头、Query 参数、请求体、客户端 IP 等;
    • Response:响应状态码、响应头、响应体、Cookie 等;
    • User:认证后的用户信息(角色、权限);
    • Items:中间件间临时共享的键值对(比如 "请求开始时间""限流计数");
    • Connection:TCP 连接信息(端口、协议)。所有中间件都通过 HttpContext 访问这些数据,不用传一堆零散参数。
  • CustomContext(扩展后)CustomContext 虽然现在是空的,但实际场景中会扩展出核心字段:

    cs 复制代码
    public 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)时,执行顺序是:

  1. 执行委托3:func3.Invoke(context)
  2. 执行委托3的前置方法:Console.WriteLine("执行中间件3的前置方法");
  3. 执行next.Invoke(context);进入委托2
  4. 执行委托2的前置方法:Console.WriteLine("执行中间件2的前置方法");
  5. 执行next.Invoke(context);进入委托1
  6. 执行委托1的前置方法:Console.WriteLine("执行中间件1的前置方法");
  7. 执行核心方法
  8. 执行委托1的后置方法:Console.WriteLine("执行中间件1的后置方法");
  9. 退回到委托2,执行委托2后置方法:Console.WriteLine("执行中间件2的后置方法");
  10. 退回到委托3,执行委托3后置方法:Console.WriteLine("执行中间件3的后置方法");
  11. 退出委托3

以上就是函数执行的全部过程,结果为:

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

相关推荐
武藤一雄5 小时前
[WPF] 万字拆解依赖属性与附加属性
前端·microsoft·c#·.net·wpf
一个帅气昵称啊5 小时前
.Net微服务网关注册和管理(基于Consul + Nginx实现)
微服务·.net·consul
武藤一雄6 小时前
C#:深入浅出委托(Delegate/Func/Action/Predicate)
开发语言·后端·microsoft·微软·c#·.net
缺点内向6 小时前
如何在Excel文档中获取分页信息
后端·c#·.net·excel
唐青枫6 小时前
C# Params Collections 详解:比 params T[] 更强大的新语法
c#·.net
武藤一雄6 小时前
.NET中到底什么是SignalR (持续更新)
后端·微软·c#·asp.net·.net·.netcore·signalr
a程序小傲6 小时前
.NET进阶——深入理解委托(1)委托入门
人工智能·后端·.net
by__csdn6 小时前
第二章 (.NET Core环境搭建)第二节( Visual Studio Code)
ide·vscode·c#·vue·asp.net·.net·.netcore
by__csdn7 小时前
第二章 (.NET Core环境搭建)第三节( Visual Studio for Mac)
ide·kubernetes·c#·asp.net·.net·.netcore·visual studio