ASP.NET Core 的 Middleware

在ASP.NET 时代,一般来说我们很少会用到HttpModule/HttpHandler,然而有些场景我们使用HttpModule/HttpHandler倒方便快捷完成我们的需求。有兴趣了解HttpModule/HttpHandler以及使用场景的话,可以看下面这个链接的内容。

选择HttpHandler还是HttpModule?

来到ASP.NET Core时代,类似功能的内容可能我们看得就要多得多了。因为在ASP.NET Core时代,微软将HttpModule“变更”之后,并为它授予了更灵活应用场景。

# 这就是这个文章要介绍的主角:Middleware(中间件)。

# Middleware

为了使用跨平台,ASP.NET Core整个架构和代码都重写了一遍,所以 HttpModule 自然也就不存在了。但是相似的功能还是有的,它的名字叫: Middleware。和以前不同,在ASP.NET Core中我们将会经常看到 Middleware的存在,因为现在的每一个服务都是用Middleware的方式呈现在ASP.NET Core 管道中。不仅如此,meddleware比起之前的HttpModule也更弹性易用了。

首先先来看看什么是middleware。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute("default",
        "{controller=Home}/{action=Index}/{id?}");
    });
}

看过ASP.NET Core项目的话,相信大家对Satarup.cs并不会陌生。在Starup.cs里面便有一个Configure()函数用于定义项目需要使用哪些middleware。

上面的例子使用了两个middleware,一个是 UseStaticFiles,另一个是 UseMvc。这两个都是core自带的middleware,所以我们可以直接使用。UseStaticFiles 是为HTTP Request提供存取网站的文件,简单理解就是使得网站上的静态文件可访问,而UseMvc就是启用MVC routing机制。有了这两个middleware,我们的的网站就有了MVC routing和读取静态文件的功能。

如果我们把UseMvc去掉,那么MVC routing也就不存在了,我们输入 http://website/[Controller]/[Action] 类似的地址也就无效了。

# 和HttpModule的不同之处

在使用HttpModule的时候,我们是在实现/重写接口,这个时候就要求我们在适当的地方做适当的事情。例如,要做 authorization 的话就最好在 HttpModule 定义好的 Authorization 事件 (AuthorizatRequest) 中完成这个功能。在 ASP.NET life cycle 的文件里我们可以查到 HttpModule 定义了那些事件,每一個事件都有哪些特別的功能。因此我们需要全面了解之后再来选择实现/重写我们需要的事件。而在Middleware中,完全没有这样的限制,也不存在这样的事件,我们可以自行设计实现我们的机制。

# Middleware 流程

https://docs.asp.net/en/latest/fundamentals/middleware.html 这个文章中说明了基本的middleware概念。目前asp.net docs里面有不少的内容都是开源社区开发者贡献的

在这个文章里面有一个简单的流程图说明了ASP.NET runtime中middleware的执行过程。

middleware执行过程

在 middleware 里面一定要定义 Invoke()函数,因为这是让 engine 默认调用 middleware 的Incoke函数。Middleware 里面所需要做什么事情就放在 Invoke() 里面,同时 Invoke() 里面还需要调用下一个 middleware。因此执行内容就如上图所示。Middleware 之间除了必须传送 HttpContext之外,也可以自定义传入其他的参数,这比以前的HttpModule方便多了。

所以当 HTTP request 进来之后,engine 便会呼叫第一个 middleware 的 Invoke(),同时把传入HttpContext,然后第一个 middleware 可以再接着呼叫第二个 middleware 的 Invoke(),同时再把 HttpContext 继续传入,一直到最后一个middleware 的 Invoke() 结束之后,整个 HttpContext 的內容可能在 middleware 里面新增或被改变了,最后再按照整個原先的 call stack 从最后一个 middleware 回到第一个 middleware,再通过 engine 回传到client 端,完成request.

下来通过一个例子我们一起来了解一下Middleware。

# 编写简单的 Middleware

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class SampleMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        if (string.IsNullOrEmpty(context.User.Identity.Name))
        {
            context.Response.Redirect("/NoName.html");
            return;
        }
        await _next.Invoke(context);
    }
}

这一个middleware的名字叫SampleMiddleware。它有一个构造函数以及Invoke函数,而Invoke()只接收一个参数HttpContext。

_next是一个叫 RequestDelegate类型,换言之这就是一个delegate,用于代表下一个middleware是谁。所以在构造函数中要把下一个middleware delegate传入。看到这里或许会觉得奇怪,我们的middleware在执行过程中怎么会知道下一个middleware是谁?这一部分稍后解释。

在 Invoke() 里面,在 await _next.Invoke() 之前都是当前middleware的逻辑代码,从上面流程图来看的话就是由左自右的方向. await _next.Invoke() 之后的代码是就是流程图上由右至右的方向,因此,透過这样简单的设计,开发者就能很明确地控制什么样逻辑要先做或后做了。

在 SampleMiddleware 之中,这里只做了一個很简单的动作,如果 username 是空白的话,就将该连接重定向到到 NoName.html 然后中断 middleware 的执行。

为了能让这个middleware作为 ApplicationBuilder来使用,我们另外需要写一个扩展方法。代码如下:

1
2
3
4
5
6
7
8
9

public static partial class MiddlewareExtensions
{
    public static IApplicationBuilder UseSampleMiddleware(
    this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SampleMiddleware>();
    }
}

这给扩展方法建立了UseSampleMiddleware(),使得我们可以让ApplicationBuilder 去读 SampleMiddleware。

这是回到Startup.cs中,在 Configure() 里面我们就可以把 SampleMiddleware 加入到我们的 pipeline中了。具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

public void Configure(IApplicationBuilder app,IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseStaticFiles();

    app.UseSampleMiddleware();   // <-- SampleMiddleware

    app.UseMvc(routes =>
    {
        routes.MapRoute("default",
        "{controller=Home}/{action=Index}/{id?}");
    });
}

把 SampleMiddleware 放在 UseStaticFiles 和 UseMvc 之间,也就是说在 http request 还沒进入到 MVC routing 之前,就会先检查 HttpContext 里面是不是有空白的 username。很显然username肯定是空白的,因为我并沒有加入任何使用者验证代码这里面,所以利用 dotnet run 來运行这个项目的时候,你就会看到 Http code 302 出現,它的意思就是 http redirect,也就是 SampleMiddleware 里面面所做的 redirect 发生作用了。

http redirect

# Middleware 的执行顺序很重要

前面解释了 middleware 执行过程是一个接着一个的.不同的 middleware 对 HttpContext 的內容都可能有不同的处理或更改,因此执行舒服便格外重要。举个例子,如果将上面 Configure() 的代码变更如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                       ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseSampleMiddleware();   // SampleMiddleware

    app.UseStaticFiles();        // StaticFiles

    app.UseMvc(routes =>
    {
        routes.MapRoute("default","{controller=Home}/{action=Index}/{id?}");
    });
}

我们把SampleMiddleware 放到StaticFiles 之前。这就导致在 SampleMiddleware 里重定向到 NoName.html会失败。

为什么会失败呢? 因为我们的 ApplicationBuilder 执行到行到 SampleMiddleware 时候重定向到NoName.html,也就是做读取静态页面,而这个功能服务方是在下一个 middleware (StaticFiles) 才会提供的,因此 ApplicationBuilder 无法找到 NoName.html,所以在浏览器上也就看不到 NoName.html 的內容。

# Middleware 这样的设计带来了很大的方便和弹性,同時我们自己也要小心 middleware 前后相依性的问题。

# Middleware 背后原理

现在 ASP.NET Core 已是开源项目了,所以最后说明一下 middleware 原理的基本概念.整個 ASP.NET fundamental 的部份用了许多的 function delegate , task, denepdency injection 的编写方法,所以要看 source code 之前,建议先对这三个东西先行了解,这样对理解 ASP.NET Core源码很有帮助.

在前面的代码中,我们看到 RequestDelegate, 顾名思义就知道这是一个delegate(委托),它是用来代表 middleware 的 delegate. 它的 source code 在 RequestDelegate.cs

IApplicationBuilder interface 是一個相当重要的接口,它定义了整個APP要用哪些服务和參數,当然也包含要使用那些 middleware,它的 souce code 在 IApplicationBuilder.cs

其中你可以看到 Use(),通过 Use() 的实例就可以把 middleware delegate 注册到 host engine 上。

另外一个就是 UseMiddlewareExtensions ,前面的代码曾用了 builder.UseMiddleware(); 它会检查你写的 middleware 是不是合法的,比如有沒有 Invoke(),是不是只有一个Invoke(),Invoke() 的参数有沒有一个是 HttpContext type,所有的检查都通过之后便建立出该middleware instance 的 delegate。

因此,当你的 ASP.NET Core APP刚启动的时候,在 Startup.cs 的 Configure() 就会把所有的 middleware delegate 建立起來,然后依序地放到內部的 stack 结构中。以上面的范例来说, stack 结构第一个元素是 StaticFiles, 然后是 SampleMiddleware 最后是 Mvc。接着每個 middleware 要被建立时是做 stack pop 的操作,所以 Mvc 的 _next 是 engine 里一些內部的 middleware 处理器,然後 pop 出 SampleMiddleware 时,就把 SampleMiddleware 的 _next 指向前面一個 pop 出來的 Mvc。依照着这样的逻辑一直到最前面的 middleware。所以在 host engine 在 Build() 之前这些动作都会完成,然后 host engine 才能执行Run()。有关 host engine 可參考 WebHostBuilder.cs

全文完。

本文整理于https://dotblogs.com.tw/aspnetshare/2016/03/20/201603191并已征得作者同意。

使用 Hugo 构建
主题 StackJimmy 设计