Skip to content
On this page

ASP.NET Core: 管道模型与中间件

在 .NetCore 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式。这导致代码的逻辑大大简化,理解中间件就要熟悉函数式编程思路。

处理请求的函数

在.NetCore 中,一次请求的完整表示是通过一个 HttpContext 对象来完成的,通过其 Request 属性可以获取当前请求的全部信息,通过 Response 可以获取对响应内容进行设置。

对于一次请求的处理可以看成一个函数,函数的处理参数就是这个 HttpContext 对象,处理的结果并不是输出结果,结果是通过 Response 来完成的,从程序调度的角度来看,函数的输出结果是一个任务 Task。

这样的话,具体处理 Http 请求的函数可以使用如下的 RequestDelegate 委托进行定义。

csharp
public delegate Task RequestDelegate(HttpContext context);

在函数参数 HttpContext 中则提供了此次请求的所有信息,context 的 Request 属性中提供了所有关于该次请求的信息,而处理的结果则在 context 的 Response 中表示。通常我们会修改 Response 的响应头,或者响应内容来表达处理的结果。

需要注意的是,该函数的返回结果是一个 Task,表示异步处理,而不是真正处理的结果。

从 .NetCore 的源代码中选取一段作为参考,这就是在没有我们自定义的处理时,.NetCore 最终的处理方式,返回 404。这里使用函数式定义。

csharp
RequestDelegate app = context =>
{
      // ......

    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
};

把它翻译成熟悉的方法形式,就是下面这个样子:

csharp
public Task app(HttpContext context)
{
    // ......

    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
};

这段代码只是设置了 Http 的响应状态码为 404,并直接返回了一个已经完成的任务对象。

为了脱离 .NetCore 复杂的环境,可以简单地进行后继的演示,我们自定义一个模拟 HttpContext 的类型 HttpContextSample 和相应的 RequestDelegate 委托类型。

在模拟请求的 HttpContextSample 中,我们内部定义了一个 StringBuilder 来保存处理的结果,以便进行检查。其中的 Output 用来模拟 Response 来处理输出。 而 RequestDelegate 则需要支持现在的 HttpContextSample。

csharp
using System.Threading.Tasks;
using System.Text;

public class HttpContextSample
{
    public StringBuilder Output { get; set; }
    public HttpContextSample() {
        Output = new StringBuilder();
    }
}
public delegate Task RequestDelegate(HttpContextSample context);

这样,我们可以定义一个基础的,使用 RequestDelegate 的示例代码。

csharp
// 定义一个表示处理请求的委托对象
RequestDelegate app = context =>
{
    context.Output.AppendLine("End of output.");
    return Task.CompletedTask;
};

// 创建模拟当前请求的对象
var context1 = new HttpContextSample();
// 处理请求
app(context1);
// 输出请求的处理结果
Console.WriteLine(context1.Output.ToString());

执行之后,可以得到如下的输出

End of output.

处理管道中间件

所谓的处理管道是使用多个中间件串联起来实现的。每个中间件当然需要提供处理请求的 RequestDelegate 支持。在请求处理管道中,通常会有多个中间件串联起来,构成处理管道。

请求处理过程

中间件和过滤器的区别

中间件和过滤器的区别:Filter是延续ASP.NET MVC的产物,同样保留了五种的Filter,分别是Authorization Filter、Resource Filter、Action Filter、Exception Filter及Result Filter,可以看出中间件和过滤器的功能类似,那么他们有什么区别?为什么又要搞一个中间件呢?其实,过滤器和中间件他们的关注点是不一样的,也就是说职责不一样,干的事情就不一样。

同作为两个AOP利器,Filter(过滤器)更贴合业务,它关注于应用程序本身,比如你看ActionFilterResultFilter,它都直接和你的ActionActionResult交互了,是不是离你很近的感觉,那我有一些比如对我的输出结果进行格式化,对我的请求的ViewModel进行数据验证啦,肯定就是用Filter无疑了。它是MVC的一部分,它可以拦截到你Action上下文的一些信息,而中间件是没有这个能力的,每一个中间件都都可以在请求之前和之后进行操作。请求处理完成之后传递给下一个请求。

那么,何时使用中间件呢?我的理解是在我们的应用程序当中和业务关系不大的一些需要在管道中做的事情可以使用,比如身份验证,Session存储,日志记录等。其实我们的 Asp.net core项目中本身已经包含了很多个中间件。比如 身份认证中间件 UseAuthorization()等。

中间件串联

如何将多个中间件串联起来呢?

可以考虑两种实现方式:函数式和方法式。

方法式就是再通过另外的方法将注册的中间件组织起来,构建一个处理管道,以后通过调用该方法来实现管道。而函数式是将整个处理管道看成一个高阶函数,以后通过调用该函数来实现管道,在 .NetCore中是使用函数式来实现请求的处理管道的。

在函数式编程中,函数本身是可以作为一个参数来进行传递的。这样可以实现高阶函数。也就是说函数的组合结果还是一个函数。

对于整个处理管道,我们最终希望得到的形式还是一个 RequestDelegate,也就是一个对当前请求的 HttpContext 进行处理的函数。

本质上来讲,中间件就是一个用来生成 RequestDelegate 对象的生成函数。

为了将多个管道中间件串联起来,每个中间件需要接收下一个中间件的处理请求的函数作为参数,中间件本身返回一个处理请求的 RequestDelegate 委托对象。所以,中间件实际上是一个生成器函数。

使用 C# 的委托表示出来,就是下面的一个类型。所以,在 .NetCore 中,中间件的类型就是这个 Func。

csharp
Func<RequestDelegate, RequestDelegate>

这个概念比较抽象,与我们所熟悉的面向对象编程方式完全不同,下面我们使用一个示例进行说明。

我们通过一个中间件来演示它的模拟实现代码。下面的代码定义了一个中间件,该中间件接收一个表示后继处理的函数,中间件的返回结果是创建的另外一个 RequestDelegate 对象。它的内部通过调用下一个处理函数来完成中间件之间的级联。

csharp
// 定义中间件
Func<RequestDelegate, RequestDelegate> middleware1 = next => {
      // 中间件返回一个 RequestDelegate 对象
    return (HttpContextSample context) => {
        // 中间件 1 的处理内容
        context.Output.AppendLine("Middleware 1 Processing.");

        // 调用后继的处理函数
        return next(context);
    };
};

把它和我们前面定义的 app 委托结合起来如下所示,注意调用中间件的结果是返回一个新的委托函数对象,它就是我们的处理管道。

csharp
RequestDelegate app = context =>
{
    context.Output.AppendLine("End of output.");
    return Task.CompletedTask;
};

// 定义中间件 1
Func<RequestDelegate, RequestDelegate> middleware1 = next =>
{
    return (HttpContextSample context) =>
    {
        // 中间件 1 的处理内容
        context.Output.AppendLine("Middleware 1 Processing.");

        // 调用后继的处理函数
        return next(context);
    };
};

// 定义中间件 2
Func<RequestDelegate, RequestDelegate> middleware2 = next =>
{

    return (HttpContextSample context) =>
    {
        // 中间件 2 的处理
        context.Output.AppendLine("Middleware 2 Processing.");

        // 调用后继的处理函数
        return next(context);
    };
};

// 构建处理管道
var step1 = middleware1(app);
var pipeline2 = middleware2(step1);
// 准备当前的请求对象
var context3 = new HttpContextSample();
// 处理请求
pipeline2(context3);
// 输出处理结果
Console.WriteLine(context3.Output.ToString());

当前的输出

Middleware 2 Processing. Middleware 1 Processing. End of output.

如果我们把这些中间件保存到几个列表中,就可以通过循环来构建处理管道。下面的示例重复使用了前面定义的 app 变量。

csharp
List<Func<RequestDelegate, RequestDelegate>> _components
    = new List<Func<RequestDelegate, RequestDelegate>>();
_components.Add(middleware1);
_components.Add(middleware2);

// 构建处理管道
foreach (var component in _components)
{
    app = component(app);
}

// 构建请求上下文对象
var context4 = new HttpContextSample();
// 使用处理管道处理请求
app(context4);
// 输出处理结果
Console.WriteLine(context4.Output.ToString());

输出结果与上一示例完全相同

Middleware 2 Processing. Middleware 1 Processing. End of output.

但是,有一个问题,我们后加入到列表中的中间件 2 是先执行的,而先加入到列表中的中间件 1 是后执行的。如果希望实际的执行顺序与加入的顺序一致,只需要将这个列表再反转一下即可。

csharp
// 反转此列表
_components.Reverse();
foreach (var component in _components)
{
    app = component(app);
}

var context5 = new HttpContextSample();
app(context5);
Console.WriteLine(context5.Output.ToString());

输出结果如下

Middleware 1 Processing. Middleware 2 Processing. End of output.

next 表示请求处理管道中的下一个中间件,处理管道会将它提供给你定义的中间件。这是将各个中间件连接起来的关键。

中间件的顺序

中间件安装一定顺寻构造成为请求处理管道,常见的处理管道如下所示: 中间件的顺序

中间件的组装流程

管道模型

Date: 2023/09/14

Authors: 张宇航

Tags: 管道、中间件、ASP.NET Core