ASP.NET Core 的 Middleware

  • 4004
  • 0
  • 2017-11-09

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 的執行過程.

middleware image from docs.asp.net

這個流程圖案說明的是在 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<SampleMiddleware>();
        }
    }

透過 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<SampleMiddleware>(); 它會檢查你寫的 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) 來完成.