这篇文章是 ASP.NET 6 中间件系列文章的第2部分,点击这里可以阅读第1部分。
在上一篇文章中,我们讨论了什么是中间件,它的作用是什么,以及在 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!");
});
接下来,让我们创建一个自定义中间件类来实现同样的效果。
中间件类的基础知识
下面是一个空类,我们将用于这个中间件:
namespace ASPNET6Middleware.Middleware
{public class SimpleResponseMiddleware{}
}
中间件类由三部分组成:
首先,任何中间件类都必须拥有RequestDelegate
类型的私有成员实例,该实例由类的构造函数填充。
RequestDelegate
代表管道中的下一个中间件:
namespace ASPNET6Middleware.Middleware
{private readonly RequestDelegate _next;public SimpleResponseMiddleware(RequestDelegate next){_next = next;}
}
其次,这个类必须有一个async方法InvokeAsync()
,它接受一个HttpContext
类型的实例作为它的第一个参数:
public class SimpleResponseMiddleware
{private readonly RequestDelegate _next;public SimpleResponseMiddleware(RequestDelegate next){_next = next;}public async Task InvokeAsync(HttpContext context){//...Implementation}
}
第三,中间件必须有自己的实现。对于这个中间件,我们要做的就是返回一个自定义的响应:
public class SimpleResponseMiddleware
{private readonly 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<>()
,因为这可以为我们提供一层更加具体的封装:
namespace ASPNET6Middleware.Extensions
{public static class MiddlewareExtensions{public static IApplicationBuilder UseSimpleResponseMiddleware(this IApplicationBuilder builder){return builder.UseMiddleware<SimpleResponseMiddleware>();}}
}
然后我们可以像这样使用这个扩展方法:
app.UseSimpleResponseMiddleware();
这两种方法都是正确的,但是我个人更喜欢扩展方法的清晰性和可读性。
构建日志中间件
在第1部分时就介绍过,中间件最常见的场景之一是日志记录,特别是记录请求路径或响应头、响应体等内容。
LoggingService 类
我们将构建一个日志中间件类,它只做两件事:
记录请求路径。
记录唯一响应头。
首先,我们必须创建接口ILoggingService
和类LoggingService
。
namespace ASPNET6Middleware.Logging
{public class LoggingService : ILoggingService{public void Log(LogLevel logLevel, string message){// 日志具体实现省略}}public interface ILoggingService{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
的实例作为参数:
namespace ASPNET6Middleware.Middleware
{public class LoggingMiddleware{private readonly RequestDelegate _next;private readonly ILoggingService _logger;public LoggingMiddleware(RequestDelegate next, ILoggingService logger){_next = next;_logger = logger;}public async Task InvokeAsync(HttpContext context){//...}}
}
我们的任务是记录请求的路径和响应的唯一头,这意味着在await next(context)
的前后都有代码:
namespace ASPNET6Middleware.Middleware
{public class LoggingMiddleware{//..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 添加到管道中
因为我更喜欢使用扩展方法来添加中间件到管道中,让我们创建一个新的扩展方法:
namespace ASPNET6Middleware.Extensions
{public static class MiddlewareExtensions{//...public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder){return builder.UseMiddleware<LoggingMiddleware>();}}
}
最后,我们需要在 Program.cs 中调用我们的新扩展方法,将LoggingMiddleware
类添加到管道中:
app.UseLoggingMiddleware();
当我们运行应用程序时,我们可以看到代码(通过断点)可以正确地记录请求路径和响应头。
在本系列的下一篇文章中,我们将讲述中间件的执行顺序,展示一个记录执行时间的新中间件,并讨论在创建中间件管道时可能发生的一些常见问题。