ASP.NET 6 中间件系列 - 自定义中间件类

在这篇文章中,我们将在这些基础上来扩展构建一些自定义中间件类。

示例项目

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

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

标准中间件结构

与我们在第1部分中所做的不同,大多数时候我们希望中间件是独立的类,而不是在 Program.cs 文件中创建。

让我们回顾一下第1部分的内容,一个响应“Hello Dear Readers!”内容的中间件?

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello Dear Readers!");
});

接下来,让我们创建一个自定义中间件类来实现同样的效果。

中间件类的基础知识

下面是一个空类,我们将用于这个中间件:

namespaceASPNET6Middleware.Middleware
{
    publicclassSimpleResponseMiddleware
    {
    }
}

中间件类由三部分组成:

首先,任何中间件类都必须拥有RequestDelegate类型的私有成员实例,该实例由类的构造函数填充。

RequestDelegate代表管道中的下一个中间件:

namespaceASPNET6Middleware.Middleware
{
    privatereadonly RequestDelegate _next;

    public SimpleResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }
}

其次,这个类必须有一个async方法InvokeAsync(),它接受一个HttpContext类型的实例作为它的第一个参数:

publicclassSimpleResponseMiddleware
{
    privatereadonly RequestDelegate _next;

    public SimpleResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //...Implementation
    }
}

第三,中间件必须有自己的实现。对于这个中间件,我们要做的就是返回一个自定义的响应:

publicclassSimpleResponseMiddleware
{
    privatereadonly RequestDelegate _next;

    public SimpleResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        await context.Response.WriteAsync("Hello Dear Readers!");
    }
}

注意:在InvokeAsync()方法中,大多数中间件都会调用await next(context);,任何不这样做的中间件都会是一个终端中间件,它代表着管道的终结,它之后的中间件都不会执行。

向管道中添加中间件

此时,我们可以使用UseMiddleware<>()方法,将这个中间件类添加到 Program.cs 文件中的 app 中:

app.UseMiddleware<LayoutMiddleware>();

对于简单的中间件类,这就足够了。

然而,我们更加常用的方式是使用扩展方法,而不是直接使用UseMiddleware<>(),因为这可以为我们提供一层更加具体的封装:

namespaceASPNET6Middleware.Extensions
{
    publicstaticclassMiddlewareExtensions
    {
        public static IApplicationBuilder UseSimpleResponseMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<SimpleResponseMiddleware>();
        }
    }
}

然后我们可以像这样使用这个扩展方法:

app.UseSimpleResponseMiddleware();

这两种方法都是正确的,但是我个人更喜欢扩展方法的清晰性和可读性。

构建日志中间件

第1部分时就介绍过,中间件最常见的场景之一是日志记录,特别是记录请求路径或响应头、响应体等内容。

LoggingService 类

我们将构建一个日志中间件类,它只做两件事:

  1. 记录请求路径。
  2. 记录唯一响应头。

首先,我们必须创建接口ILoggingService和类LoggingService

namespaceASPNET6Middleware.Logging
{
    publicclassLoggingService : ILoggingService
    {

        public void Log(LogLevel logLevel, string message)
        {
            // 日志具体实现省略
        }
    }

    publicinterfaceILoggingService
    {
        public void Log(LogLevel level, string message);
    }
}

然后我们需要在 Program.cs 中将LoggingService添加到应用程序的Services集合中:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddTransient<ILoggingService, LoggingService>();

中间件类也像普通类一样可以注入服务。

LoggingMiddleware 类

现在我们需要一个LoggingMiddleware类来做日志。

首先,让我们为LoggingMiddleware类创建好中间件结构,它在构造函数中接受一个ILoggingService的实例作为参数:

namespaceASPNET6Middleware.Middleware
{
    publicclassLoggingMiddleware
    {
        privatereadonly RequestDelegate _next;
        privatereadonly ILoggingService _logger;

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

        public async Task InvokeAsync(HttpContext context)
        {
            //...
        }
    }
}

我们的任务是记录请求的路径和响应的唯一头,这意味着在await next(context)的前后都有代码:

namespaceASPNET6Middleware.Middleware
{
    publicclassLoggingMiddleware
    {
        //..

        public async Task InvokeAsync(HttpContext context)
        {
            // 记录传入的请求路径
            _logger.Log(LogLevel.Information, context.Request.Path);

            // 调用管道中的下一个中间件
            await _next(context);

            // 获得唯一响应头
            var uniqueResponseHeaders 
                = context.Response.Headers
                                  .Select(x => x.Key)
                                  .Distinct();

            // 记录响应头名称
            _logger.Log(LogLevel.Information, string.Join(", ", uniqueResponseHeaders));
        }
    }
}

将 LoggingMiddleware 添加到管道中

因为我更喜欢使用扩展方法来添加中间件到管道中,让我们创建一个新的扩展方法:

namespaceASPNET6Middleware.Extensions
{
    publicstaticclassMiddlewareExtensions
    {
        //...
    
        public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<LoggingMiddleware>();
        }
    }
}

最后,我们需要在 Program.cs 中调用我们的新扩展方法,将LoggingMiddleware类添加到管道中:

app.UseLoggingMiddleware();

当我们运行应用程序时,我们可以看到代码(通过断点)可以正确地记录请求路径和响应头。

ASP.NET 6 中间件系列 - 自定义中间件类

在本系列的下一篇文章中,我们将讲述中间件的执行顺序,展示一个记录执行时间的新中间件,并讨论在创建中间件管道时可能发生的一些常见问题。