3.2.2 添加中间件

在上一节中,我们提到了Startup类的Configure方法,该方法就是添加中间件的地方。在Configure方法中,通过调用IApplicationBuilder接口中以Use开头的扩展方法,即可添加系统内置的中间件,如下所示:

public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error");
    app.UseStaticFiles();
    app.UseAuthentication();
    app.UseMvc();
}

上述代码中每一个以Use开头的方法都会逐一并顺序地向管道添加相应的中间件。这里需要特别注意,中间件的添加顺序将决定HTTP请求以及HTTP响应遍历它们的顺序。因此,对于上面的代码,传入的请求首先会经过异常处理中间件,再到静态文件中间件,接着是认证中间件,最后则是MVC中间件。每一个中间件都可以终止请求管道,例如,如果认证失败,则不再继续向后执行。

这些以Use开头的方法都是扩展方法,它们封装了一些细节。而在每一个扩展方法的内部实现中,每个中间件都是通过调用IApplicationBuilder接口的Use和Run方法添加到请求管道中的。

下面的例子是使用Run方法来添加一个中间件,该中间件会输出与本次请求相关的信息。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Run(async (context) =>
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("---- REQUEST ----");
        sb.AppendLine($"Host: {context.Request.Host}");
        sb.AppendLine($"Method: {context.Request.Method}");
        sb.AppendLine($"Path: {context.Request.Path}");
        sb.AppendLine($"Protocol: {context.Request.Protocol}");
        foreach (var item in context.Request.Headers)
        {
            sb.AppendLine($"  {item.Key}: {item.Value}");
        }
        await context.Response.WriteAsync(sb.ToString());
    });
}

当请求https://localhost:5001时,则会输出类似如下内容:

---- REQUEST ----
Host: localhost:5001
Method: GET
Path: /
Protocol: HTTP/1.1
  Cache-Control: no-cache
  Connection: keep-alive
  Accept: */*
  Accept-Encoding: gzip, deflate
  Host: localhost:5001
  User-Agent: PostmanRuntime/7.4.0

Run方法接受一个RequestDelegate类型的参数,它是一个委托,用来处理传入的HTTP请求,它的定义如下:

public delegate Task RequestDelegate(HttpContext context);

由于它接受一个HttpContext类型的参数,并返回Task类型,因此可以使用如下Lambda表达式向Run方法传递参数。

app.Run(async context => { … } );

之后,通过HttpContext对象的Request属性可以得到表示当前HTTP请求的对象,并最终使用其Response属性的WriteAsync方法输出结果,从而结束本次请求。

与Run方法不同的是,Use方法在处理完请求后还会将请求传入下一个中间件,并由它继续处理。它接受的参数类型为Func<HttpContext, Func<Task>, Task>,因此可以使用如下Lambda表达式向Use方法传递参数。

app.Use(async (context,next) => {});

其中,next表示下一个中件间,它是一个异步方法,应该在当前中间件中调用它。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        Console.WriteLine("中间件A:开始");
        await next();
        Console.WriteLine("中间件 A:结束");
    });
}

在调用下一个中间件的前后位置时,可以添加处理当前请求的代码。当执行上述代码时,控制台窗口会顺序地输出相应的日志。然而,由于上面的例子中只添加了一个中间件,并没有后续的中间件处理HTTP请求并返回HTTP响应内容,因此请求结束后返回的响应状态码为404 Not Found。如果一个请求所有中间件都没处理,则将返回404 Not Found状态码。要解决这一问题,可以继续添加中间件,也可以不调用下一个中间件,而在当前中间件中直接输出响应,以结束请求管道。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        var timer = System.Diagnostics.Stopwatch.StartNew();
        Console.WriteLine("中间件 A:开始,{0}", timer.ElapsedMilliseconds);
        await next();
        Console.WriteLine("中间件 A:结束,{0}", timer.ElapsedMilliseconds);
    });
    app.Run(async (context) =>
    {
        Console.WriteLine("中间件 B");
        await Task.Delay(500);
        await context.Response.WriteAsync("Hello, world");
    });
}

上述代码的输出结果如下:

中间件 A:开始,0
中间件 B
中间件 A:结束,534

除了Run和Use方法外,IApplicationBuilder接口还提供了Map、MapWhen及UseWhen方法,它们都可以指定条件,并在条件满足时创建新的分支管道,同时在新的分支上添加并执行中间件,定义如下:

public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);
public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration);
public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration);

其中,Map会根据是否匹配指定的请求路径来决定是否在一个新的分支上继续执行后续的中间件,并且在新分支上执行完后,不再回到原来的管道上,MapWhen则可以满足更复杂的条件,它接收Func<HttpContext, bool>类型的参数,并以该参数作为判断条件,因此,它会对HttpContext对象进行更细致的判断(如是否包含指定的请求消息头等),然后决定是否进入新的分支继续执行指定的中间件。而UseWhen与MapWhen尽管接受的参数完全一致,但它不像Map和MapWhen一样,由它创建的分支在执行完后会继续回到原来的管道上。下面的例子说明了Map方法的功能。

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        Console.WriteLine("中间件 A:开始");
        await next();
        Console.WriteLine("中间件 A:结束");
    });
    app.Map(
        new PathString("/maptest"),
        a => a.Use(async (context, next) =>
        {
            Console.WriteLine("中间件 B:开始");
            await next();
            Console.WriteLine("中间件 B:结束");
        }));
    app.Run(async context =>
    {
        Console.WriteLine("中间件 C");
        await context.Response.WriteAsync("Hello world");
    });
}

当请求https://localhost:5001/maptest时,控制台中将会输出以下结果。

中间件 A:开始
中间件 B:开始
中间件 B:结束
中间件 A:结束

上述请求的响应码是404 Not Found,这是因为在新分支的中间件B中调用了并不存在的中间件,并且最后添加的要输出“中间件C”的中间件也没有执行。而如果将上例中Map方法改为UseWhen方法:

app.UseWhen(
    context => context.Request.Path.Value == "/maptest",
    a => a.Use(async (context, next) =>
    {
        Console.WriteLine("中间件 B:开始");
        await next();
        Console.WriteLine("中间件 B:结束");
}));

同样请求https://localhost:5001/maptest,则所有添加的中间件都会执行,并且其响应码为200 OK,控制台的输出结果如下所示。

中间件A:开始
中间件B:开始
中间件C
中间件B:结束
中间件A:结束