ASP.NET Core自定义中间件的方式

ASP.NET Core应用本质上,其实就是由若干个中间件构建成的请求处理管道。管道相当于一个故事的框架,而中间件就相当于故事中的某些情节。同一个故事框架采用不同的情节拼凑,最终会体现出不同风格的故事。而我们的ASP.NET Core应用也正是如此,同一管道采用不同的中间件组合,最终也会呈现出不同的应用形态。

从上述的概念种可以看出,中间件在ASP.NET Core应用有着举足轻重的地位。虽然ASP.NET Core为我们提供了一组丰富的内置中间件,但有些时候我们可能会需要自定义一些中间件,将其穿插到管道中,以便满足我们特定业务场景的需求,所以本文将介绍3种方式来满足自定义中间件的需求。


1.委托形式

在应用程序代码中,我们可以从用于注册中间件的Use方法中看出,所谓管道中的中间件其实就是一种委托类型的对象,这个具体的委托对象体现为Fun<RequestDelegate,RequestDelegate>”。

从Fun<RequestDelegate,RequestDelegate>委托的定义可以看出,该委托类型的入参和返回值都是一个RequestDelegate委托类型的对象。RequestDelegate委托类型其实就是管道在代码中的体现形式,该委托类型承载很多关于请求响应的重要信息,定义如下:

public delegate Task RequestDelegate(HttpContext context);

 Fun<RequestDelegate,RequestDelegate>委托中,入参的RequestDelegate对象表示由上一个中间件构建的管道,返回值的RequestDelegate对象表示:将当前中间件基于上一个管道处理后生成的新管道。由于中间件体现为一个Fun<RequestDelegate,RequestDelegate>委托对象,那么这就代表我们可以定义一个与该委托具有一致声明的方法作为自定义中间件的方式。具体的代码实现方式如下:

 1 //创建应用
 2 var app = WebApplication.Create(args);
 3 
 4 //转换获得应用建造者
 5 IApplicationBuilder appBuilder = app;
 6 
 7 //注册自定义的中间件
 8 appBuilder.Use(SayHi);
 9 
10 //运行应用
11 app.Run();
12 
13 //定义为Fun<RequestDelegate,RequestDelegate>类型的方法
14 static RequestDelegate SayHi(RequestDelegate  request)
15     => httpContext => httpContext.Response.WriteAsync("Hello");

上面的代码是在一个原始的控制台程序中编写的,并且自行进行了主机应用的构建。在代码中定义了一个和Fun<RequestDelegate,RequestDelegate>委托签名一致的SayHi方法,并以此方法作为中间件进行了引用。虽然这是一个可行的方式,但在实际开发的工作场景中,其实很少会使用委托形式作为自定义中间件的方式。在此处之所以演示这种形式,主要是为了表面中间件本质是一个委托,并且不管通过什么形式去定义中间件,它最终都会体现为一个Fun<RequestDelegate,RequestDelegate>委托对象。


2.强类型中间件

在实际的开发过程中,基本上都会将自定义的中间件定义为一个具体类型,而对于使用强类型的中间件而言,则我们定义的中间件类型必须实现IMiddleware接口。既然通过一个具体类型来定义中间件,类型在使用上则势必会与其他类型产生依赖关联性,那么对于中间件类型中依赖服务的实例化,框架则要求我们使用依赖注入的方式。接下来我们将通过代码示例演示如何定义一个强类型的中间件。

 2.1.定义中间件的依赖

下面代码定义的类型是我们预先为中间件类型定义的依赖项,ISeasonTips接口类型的作用主要是,根据不同月份获取对应的季节,并输出对应季节的注意事项,其中SeasonTips类型是接口的默认实现。

    public interface ISeasonTips
    {
        string Prompt(DateTimeOffset time);
    }

    public class SeasonTips : ISeasonTips
    {
        //根据不同月份提示季节注意事项
        public string Prompt(DateTimeOffset time) => time.Month switch
        {
            var h when h >= 3 && h <= 5 => "春天到了,早晚温差比较大,要注意别感冒。",
            var h when h >= 6 && h <= 8 => "夏天到了,天气炎热,要注意别防嗮。",
            var h when h >= 9 && h <= 11 => "秋天到了,天气干燥,要注意多喝水。",
            _ => "冬天到了,天气寒冷,要注意防寒保暖。"

        }; //END Prompt()  

    }

2.2.定义中间件类型

下面的代码中,我们定义了一个名为SeasonMiddleware的中间件类型,并实现IMiddleware接口。该中间件的处理请求的逻辑在InvokeAsync方法中,该方法调用其依赖类型的Prompt方法,根据当前时间获取当前季节的注意事项进行输出。在该调用该方法后,我们还对InvokeAsync的另一个参数:“RequestDelegate类型的委托对象”进行了调用,以便执行管道中的下一个中间件。另外,对于中间件依赖的类型ISeasonTips,我们将其定义在构造函数的参数列表上,以便依赖注入容器提供相应的实例。

 1    /// <summary>
 2     /// 强类型中间件
 3     /// </summary>
 4     public class SeasonMiddleware : IMiddleware
 5     {
 6         //依赖类型,通过构造函数进行依赖注入
 7         private readonly ISeasonTips _seasonTips;
 8         public SeasonMiddleware(ISeasonTips seasonTips)
 9         {
10             _seasonTips = seasonTips;
11         }
12 
13         //调用依赖的“季节提示类型”,根据当前时间获取当前季节的注意事项,并进行响应输出
14         public async Task InvokeAsync(HttpContext context, RequestDelegate next)
15         {
16             await context.Response.WriteAsync(_seasonTips.Prompt(DateTimeOffset.Now));
17 
18             //调用管道中的下一个中间件
19             await next(context);
20         }  // END InvokeAsync()
21 
22     }  // END Class

2.3.应用中间件

在下面的代码中我们对自定义的“强类型中间件”进行了应用。由于“强类型中间件”的实例以及依赖都是由依赖注入容器提供的,所以不仅要对依赖的服务进行注册,还要对自身的中间件类型进行服务注册。在服务注册之后,我们使用WebApplication对象的UseMiddleware<SeasonMiddleware>扩展方法,将该中间件添加到应用程序的请求管道中。由于在该中间件后没有其他中间件的处理,所以我们通过调用Run扩展方法注册了管道末端的中间件,以便结束当前请求,将响应输出到客户端。

 1 using dotNet6Demo;
 2 
 3 //创建“应用建造者”
 4 var builder = WebApplication.CreateBuilder(args);
 5 
 6 //服务注册
 7 builder.Services.AddSingleton<ISeasonTips, SeasonTips>().AddSingleton<SeasonMiddleware>();   
 8 
 9 //构建应用
10 var app = builder.Build();
11  
12 //引用强类型中间件
13 app.UseMiddleware<SeasonMiddleware>();
14 
15 //末端的中间件
16 app.Run(async (context) =>
17 {
18     await context.Response.WriteAsync("请求结束");
19 });
20 
21 //运行应用
22 app.Run();

到目前为止,结合本示例以上的3个步骤,启动运行程序就可以验证自定义强类型中间件的效果了。


3.基于约定的中间件

对于ASP.NET的开发者而言,基于约定的编程模式应该不会陌生。例如在ASP.NET MVC框架中,“Action”默认查找视图就有一种基于约定的规则,即“Action”首先会在Views目录中查找与当前“Controller”同名的目录,然后在该目录中查找与“Action”同名的视图文件。这种基于约定的设计方式,在自定义中间件领域也同样使用到了,即基于约定的中间件。

3.1.约定规则

基于约定的中间件它不必像强类型中间件那样,必须实现IMiddleware接口或继承某些基类,它只用按照框架约定的方式定义中间件类型即可,具体的约定规则如下:

  1. 中间件类型必须要定义为一个公共的、可供外界实例化的类型,静态类型无效;
  2. 构造函数的参数中必须包含RequestDelegate类型,如果存在依赖类型则也必须包含在构造函数中;
  3. 必须定义InvokeAsync或Invoke方法,方法签名为:public Task Invoke(HttpContext context);

对以上的约定进行一个补充说明:构造函数的参数列表要包含依赖的类型,是为了依赖注入容器对依赖类型提供实例;RequestDelegate参数具有传递性,表示由后续中间件构建的管道,当前中间件利用它将请求转交给后续管道进行处理。InvokeAsync或Invoke方法主要是代表中间件在管道中处理请求的逻辑。

3.2.应用实现

下面我们在“强类型中间件”示例的基础上,根据约定规则将SeasonMiddleware类型改造为“基于约定的中间件”,代码如下:

 1     /// <summary>
 2     /// 基于约定的中间件
 3     /// </summary>
 4     public class SeasonMiddleware
 5     {
 6         private readonly ISeasonTips _seasonTips;
 7         private readonly RequestDelegate _next;
 8 
 9         public SeasonMiddleware(ISeasonTips seasonTips, RequestDelegate next)
10         {
11             _seasonTips = seasonTips;
12             _next = next;
13         }
14 
15         //调用依赖的“季节提示类型”,根据当前时间获取当前季节的注意事项,并进行响应输出
16         public async Task InvokeAsync(HttpContext context)
17         {
18             await context.Response.WriteAsync(_seasonTips.Prompt(DateTimeOffset.Now));
19             //调用管道中的下一个中间件
20             await _next(context);
21 
22         }  // END InvokeAsync()
23 
24     }  // END Class

在中间件引用方面,“基于约定的中间件”同样可以使用“app.UseMiddleware<SeasonMiddleware>()”的方式进行引用,但是在此我们介绍一种较为常用的方式,就是将自定义中间件的引用方式进行封装,将其作为IApplicationBuilder类型的扩展方法来使用,扩展方法定义的代码如下:

1     public static class SeasonMiddlewareExtensions
2     {
3         public static IApplicationBuilder UseSeason(this IApplicationBuilder builder)
4         {
5             return builder.UseMiddleware<SeasonMiddleware>();
6         }
7     }

接下来在示例应用方面,将其调整为使用“基于约定中间件”的形式,并使用扩展方法引用中间件。

 1 using dotNet6Demo;
 2 
 3 //创建“应用建造者”
 4 var builder = WebApplication.CreateBuilder(args);
 5 
 6 //服务注册
 7 builder.Services.AddSingleton<ISeasonTips, SeasonTips>();   
 8 
 9 //构建应用
10 var app = builder.Build();
11 
12 //通过自定义扩展方法 引用中间件
13 app.UseSeason();
14 
15 //末端的中间件
16 app.Run(async (context) =>
17 {
18     await context.Response.WriteAsync("请求结束");
19 });
20 
21 //运行应用
22 app.Run();

3.3.提供方式

在对以上中间件应用方面,我们能可以看出“基于约定的中间件”类型并没有进行服务注册,而“强类型中间件”类型却进行了服务注册,这是因为两者在提供实例的方式上有着本质的区别。

“基于约定的中间件”的实例是在应用启动时便可提供的,并且只能指定的一个固定的生命周期模式“Singleton”,所以该类型中间件具有和应用程序一样的生存期,直到应用程序关闭才会释放。

“强类型中间件”的实例并不是在应用启动时提供的,它需要根据服务注册时指定的生命周期,来决定创建提供的时机。例如“强类型中间件”注册的生命周期为“Scoped”,那么依赖注入容器会根据客户端的请求实时创建中间件的实例,请求处理完成后才会被释放。


总结

中间件的使用地位在ASP.NET Core中绝对是毋庸置疑的,那么对于较为复杂的项目而言,自定义中间件的需求绝对是“绕不开的弯”,所以我们必须掌握自定义中间件的方式。

本文介绍了3种可以实现自定义ASP.NET Core中间件的方式。其中第一种并不推崇作为实战运用的手段,其目的是为了让我们明白:中间件最终的体现形式其实就是一个委托对象,该委托对象承载了请求上下信息,并具有传递性。在实际的使用中,我们可以在第二种和第三种中进行选择,也就是“强类型中间件”和“基于约定的中间件”,从两者的特点上来看,“基于约定的中间件”在使用方面会更加的方便,但是其生命周期模式只能局限于Singleton。而“强类型中间件”可以通过服务注册为中间件实例指定任意的生命周期模式,相比更加灵活。

对于具体的选择,我们想我们还是交给我们实际的运用场景。

如果想了解更多关于自定义 ASP.NET Core 中间件的方式,可以访问如下的官方文档:

写入自定义 ASP.NET Core 中间件 | Microsoft Docs

posted @ 2022-08-19 15:01  姜承轩  阅读(1334)  评论(1编辑  收藏  举报