ASP.NET 6 中间件系列 - 执行顺序

这篇文章是 ASP.NET 6 中间件系列文章的第 3 部分,你还可以阅读第1部分第2部分

我们通过中间件创建的管道是有执行顺序的,执行顺序与中间件的添加顺序是相同的,接下来我们讨论一下为什么要有执行顺序,以及它的重要性。.

示例项目

在 GitHub 上可以获得这篇文章涉及到的代码:

https://github.com/zilor-net/ASPNET6Middleware/tree/Part3

执行顺序

在本系列的第1部分中,中间件构成了一个管道,该管道中的中间件按照一定的顺序执行,如下图所示:

ASP.NET 6 中间件系列 - 执行顺序

请求按顺序经过各个中间件,而响应则按相反的顺序返回。

在前面的文章中,我们已经定义了两个中间件类:

  • LoggingMiddleware用于记录请求/响应日志;

  • SimpleResponseMiddleware用于中断管道,返回响应。

在这篇文章中,我们仍然以LoggingMiddleware为例:

app.UseLoggingMiddleware();

添加延迟

我们创建一个新的中间件类,叫做IntentionalDelayMiddleware,它看起来像这样:

namespace MiddlewareNET6Demo.Middleware
{
    public class IntentionalDelayMiddleware
    {
        private readonly RequestDelegate _next;
        public IntentionalDelayMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        public async Task InvokeAsync(HttpContext context)
        {
            await Task.Delay(100);
            await _next(context);
            await Task.Delay(100);
        }
    }
}

这个中间件在处理传入请求和处理传出响应时都会等待 100ms,总等待时间为 200ms。

当然,实际场景下,我们并不会这么做。

在这里,IntentionalDelayMiddleware只是代表了某种未定义的中间件,它需要一个可预测的时间来执行。

我们需要向管道中添加一个IntentionalDelayMiddleware的实例。问题在于,我们是在LoggingMiddleware之前还是之后添加它?

其实在这种情况下,这个问题可能并不重要,因为这两个中间件不会进行交互,也不处理相同的事情。

在这个示例中,让我们在LoggingMiddleware之后添加IntentionalDelayMiddleware

app.UseLoggingMiddleware();
app.UseIntentionalDelayMiddleware();

如果现在运行应用程序,我们可能不会发现明显的差异,因为 200 毫秒相当快。

添加执行时间中间件

为了监视每个请求的所消耗的时间,我们往往需要记录每个请求到我们系统的执行时间。

这个需求对于中间件来说是非常简单的,我们可以使用 .NET 提供的Stopwatch类和第2篇文章中创建的LoggingService来实现。

下面是名为TimeLoggingMiddleware的中间件类:

using MiddlewareNET6Demo.Logging;
using System.Diagnostics;

namespace MiddlewareNET6Demo.Middleware
{
    public class TimeLoggingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILoggingService _logger;

        public TimeLoggingMiddleware(RequestDelegate next, 
                                     ILoggingService logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            await _next(context);

            watch.Stop();
            _logger.Log(LogLevel.Information, "Time to execute: " + watch.ElapsedMilliseconds + " milliseconds.");
        }
    }
}

我们需要将其添加到管道中。但是,这里仍然有个问题:我们应该添加到哪个位置?

如果我们将TimeLoggingMiddleware添加到IntentionalDelayMiddleware之前,那么后者所引起的延迟将包含在前者所度量的范围中。

如果我们将TimeLoggingMiddleware添加到IntentionalDelayMiddleware之后,那么后者所引起的延迟将不会包含在前者所度量的范围中。

让我们来看看管道:

app.UseHttpsRedirection();
app.UseStaticFiles();

// 如果该中间件发生任何延迟,那么该延迟不会包含在时间日志中。
app.UseLoggingMiddleware();

// 时间记录中间件
app.UseTimeLoggingMiddleware();

// 延迟中间件。
// 此时,延迟被包含在时间日志中。
app.UseIntentionalDelayMiddleware();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

在这个 Program.cs 文件中,哪个位置更适合放置TimeLoggingMiddleware?其答案取决于几个问题:

  • 时间日志需要包括诸如无效授权之类的执行时间吗?如果是,那么必须在调用app.UseAuthorization()之前,放置TimeLoggingMiddleware
  • 路由调用需要的时间非常少,但可以测量。我们要把它包括进去吗?如果是,就必须在调用app.UseRouting()之前,放置TimeLoggingMiddleware

像大多数现实世界的问题一样,这个问题没有明确的答案。

如果没有明确的指示,那么这最终需要由开发人员根据系统的具体情况来做出决定。

需要注意的是:

app.UseIntentionalDelayMiddleware();
app.UseTimeLoggingMiddleware();

这两个是完全不同的:

app.UseTimeLoggingMiddleware();
app.UseIntentionalDelayMiddleware();

这就是为什么中间件在管道中顺序很重要的一个例子。