ASP.NET Core 的 Middleware

HttpModule/HttpHandler 从 ASP.NET 1.0 开始就存在于整个 ASP.NET 的结构中,只不过一般的使用者比较少注意到它们,因此它们被使用的频率和高阶层的 web form 来比就少了很多.然而在 ASP.NET Core 开始,你就很难不会注意到它们了,因为它们就直接存在于你所要执行的程序,直接就看的到,所以不注意也不行了.这一篇文章在说明在 ASP.NET Core 中是怎么使用类似像在以前版本 HttpModule 的功能,在 ASP.NET Core 里,它不再叫  HttpModule/HttpHandler 了,有了新的名字,叫 Middleware.


HttpModule

如果你曾经或现在正在工作于 HttpModule,那表示你对 4.x 版前的 ASP.NET life cycle 比较清楚了.有关 ASP.NET life cycle ,可以参考 ASP.NET life cycle - https://msdn.microsoft.com/en-us/library/ms178473(v=vs.85).aspx

我先分享我之前用 HttpModule 的经验,大约十多年前左右,当时刚好是 ASP.NET 2.0 将要上市的时间,所以那时候很多的项目还是用 ASP.NET 1.0 来写. 因为 ASP.NET 1.0 并还没有完整的 access control 机制,只有较为单纯的 windows authentication 和 form authentication 等,而在 authorization 在 ASP.NET 1.0 中都是空白的.因此,当时有个同事就利用了 HttpModule 自行处理了 authorization 的部分,也就是说当使用者透过了 Windos authentication/Form authentication 之后,就能在 HttpContext 中得到 username,同时在 HttpContext 中也包含该连线要去的 web form (.aspx),所以在那一个 HttpModule 之中就可以验证该使用者是否有权限存取该网页.透过存取数据库中的数据来做为验证,一旦发现无权限时便会把该 http request 重新导向到某一个显示错误消息的网页.透过这样做的好处就是 authorization 的工作就不用放在每一个网页 (*.aspx) 中来检查,大大地减少了其他工程师的负担,让其他工程师只要专注在该网页所需要的功能即可,不用担心权限处理的事情.后来,我把这个想法重新包装起来,然后放在 codeplex 上做为一个 open source 让所有人可以来参考,其网址是 http://aspnetaccesscontrol.codeplex.com,后来因为 ASP.NET 有较完整的存取控制机制了,因此从 2009 年之后我就再也没继续维护那一项 open source project.

Middleware

在 ASP.NET Core 中,由于整个架构和程序都是重新来了,所以 HttpModule 自然也就不存在了.但是相似的功能还是有的,它的名字叫 Middleware.跟以前不同的是在 ASP.NET Core 中你一定会看到 Middleware 的存在,因为现在每一个服务都是用 middleware 的方式呈现在 ASP.NET Core 的 pipleline 中.不尽如此,middleware 便得更加弹性易用,跟以前 HttpModule 比起来方便多了.首先,先来看什么是 middleware.

        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 的测试版,相信你对 Startup.cs 并不会陌生.在 Startup.cs 里面有一个 Configure() mehtod 就是用来定义要使用那些 middleware.上面的例子使用了两个 middleware,一个是 UseStaticFiles,另一个是 UseMvc,这两个都是内建的 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 里面有不少的内容都是社群成员所贡献的,middleware 这一篇内容就是.在这篇文档里有一个简易的流程图可以用来说明 middleware 的执行过程.

这个流程图案说明的是在 ASP.NET runtime 时期 middleware 的执行过程.在 middleware 里一定要定义一个 method 叫 Invoke(),因为这是让 engine 可以调用该 middleware 的进入点.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 端.接下来,直接来看一个简单的例子就能让你了解更细节.

撰写简单的 Middleware 

下面的程序是一个非常简单的 middleware

    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.它有一个 constructor 和一个 Invoke() method,而 Invoke() 只接收一个参数 HttpContext._next 代表的是一个特别设计的东西,叫 RequestDelegate,它是一个 delegate 用来代表下一个 middleware 是谁,所以在 constructor 里就要把下一个 middleware delegate 给带进来.也许你会觉得奇怪,执行的过程中这个 middleware 怎么会知道下一个 middleware 是谁呢 ? 这部分稍后会说明.

在 Invoke() 里面,在 await _next.Invoke() 之前都是在调用下一个 middleware 时会执行的程序,从上面流程图来看的话就是由左自右的方式. await _next.Invoke() 之后的程序是就是流程图上由右至右的方向,因此,透过这样简单的设计,开发者就能很明确地控制什么样的程序要先做或后做了.在 SampleMiddleware 之中,我只做了一个很简单的动作,如果 username 是空白的话,就将该连线重新导向到 NoName.html 然后中断 middleware 的执行.

为了要让这个 middleware 可以让 ApplicationBuilder 来使用,我们另外建立以下的程序

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

透过 C# extension method,建立  UseSampleMiddleware(),而里面的程序就是让 ApplicationBuilder 去读 SampleMiddleware.

接着回到 Startup.cs,在 Configure() 里把 SampleMiddleware 加入到程序的 pipeline.

        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.很显然一定会是的,因为我并没有加入任何使用者验证的程序在这项目里,所以利用 dotnet run 来执行这个项目时,你就会看到 Http code 302 出现,它的意思就是 http redirect,也就是 SampleMiddleware 里面所做的 redirect 发生作用了.

Middleware 的执行顺序很重要

前面解释了 middleware 执行的过程,都是一个接着一个.不同的 middleware 对 HttpContext 的内容都可能有不同的改变,因此执行的顺序就显得格外重要.举个例子,如果将上面 Configure() 的程序更动如下:

        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 里面做了 http redirect 到 NoName.html 将会失败,为什么会失败呢 ? 因为 ApplicationBuilder 执行到 SampleMiddleware 时就要做连线静态网页的功能,而这个功能是在下一个 middleware (StaticFiles) 才会有的,因此 ApplicationBuilder 无法找到 NoName.html,所以在浏览器上就看不到 NoName.html 的内容.

Middleware 这样的设计带来很大的方便和弹性,同时开发者自己也要小心 middleware 前后相依性的问题.

Middleware 背后原理

由于现在 ASP.NET Core 已是 open source 了,所以最后来说明一下 middleware 原理的基本概念.整个 ASP.NET fundamental 的部分用了许多 function delegate , task, denepdency injection 的撰写手法,所以要看 source code 之前,建议先对这三个东西先行了解才能对看 ASP.NET Core 的 source code 有帮助.

在前面的程序中,你看到 RequestDelegate,  顾名思义就知道这是一个 delegate,它是用来代表 middleware 的 delegate. 它的 source code 在 https://github.com/aspnet/httpabstractions/blob/master/src/Microsoft.AspNet.Http.Abstractions/RequestDelegate.cs

IApplicationBuilder interface 是一个相当重要的界面,它定义了整个程序要用到那些服务和参数,当然也包含要使用那些 middleware,它的 souce code 在 https://github.com/aspnet/httpabstractions/blob/master/src/Microsoft.AspNet.Http.Abstractions/IApplicationBuilder.cs,其中你可以看到 Use(),透过 Use() 的实践就可以把 middleware delegate 注册到 host engine 上.

另外一个就是 UseMiddlewareExtensions ,前面的程序范例曾用了 builder.UseMiddleware(); 它会检查你写的 middleware 是不是对的,比如有没有 Invoke(),是不是只有一个 Invoke(),Invoke() 的参数有没有一个是 HttpContext type,检查都通过时便建立出该 middleware instance 的 delegate.

因此,当你的 ASP.NET Core 程序刚启动时,在 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 可参考 https://github.com/aspnet/hosting/blob/master/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs

在写这篇文章时,ASP.NET Core RC2 还尚未上市,因此若你想试以上这些功能的话,Visual Studio 只能当成一个基本的程序编辑器了,restore, build 和 run 的动作还是要透过 DotNet CLI (https://github.com/dotnet/cli) 来完成.