1. 程式人生 > >解讀ASP.NET 5 & MVC6系列(6):Middleware詳解

解讀ASP.NET 5 & MVC6系列(6):Middleware詳解

在第1章專案結構分析中,我們提到Startup.cs作為整個程式的入口點,等同於傳統的Global.asax檔案,即:用於初始化系統級的資訊(例如,MVC中的路由配置)。本章我們就來一一分析,在這裡如何初始化這些系統級的資訊。

新舊版本之間的Pipeline區別

ASP.NET 5和之前版本的最大區別是對HTTP Pipeline的全新重寫,在之前的版本中,請求過濾器的通常是以HttpModule為模組元件,這些元件針對HttpApplication裡定義的各個週期內的事件進行響應,從而用於實現認證、全域性錯誤處理、日誌等功能。傳統的Form表單認證就是一個HTTPModuleHTTPModule

不僅能夠過濾Request請求,還可以和Response響應進行互動並修改。這些HTTPModule元件都繼承於IHttpModule介面,而該介面是位於System.Web.dll中。

HttpModule程式碼不僅可以在Global.asax中的各事件週期中進行新增,還可以單獨編譯成類庫並在web.config中進行註冊。

新版的ASP.NET 5拋棄了重量級的System.Web.dll,相應地引入了Middleware的概念,Middleware的官方定義如下:

Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.
在伺服器和應用程式之間的管線Pipeline之間,針對特定的目的,穿插多個Middleware元件,從而對request請求和response響應進行檢
查、路由、或修改。

該定義和傳統的HttpModule以及HttpHandler特別像。

Middleware的註冊和配置

在ASP.NET5中,request請求管線(Pipeline)的訪問是在Startup類中進行的,該類時一個約定類,並且裡面的ConfigureServices方法、Configure方法、以及相應的引數也是事先約定的,所以不能進行改動。

Middleware中的依賴處理:ConfigureServices方法

在ASP.NET5中的各種預設的Middleware中,都使用了依賴注入的功能,所以在使用Middleware中的功能時,需要提前將依賴注入所需要的型別及對映關係都註冊到依賴注入管理系統中,即IServiceCollection集合,而ConfigureServices方法接收的就一個IServiceCollection型別的引數,該引數就是所有註冊過型別的集合,通過原生的依賴注入元件進行管理(關於ASP.NET5中的依賴注入,我們會在單獨章節中進行講解),在該方法內,我們可以向該集合中新增新的型別和型別對映關係,示例如下:

// Add MVC services to the services container.
services.AddMvc();

示例中的程式碼用於向系統新增Mvc模組相關的Service型別以支撐MVC功能,該方法是一個擴充套件方法,用於在集合中新增與MVC相關的多個型別。

Middleware的註冊和配置:Configure方法

Configure方法的簽名如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
    // ...
}

Configure方法接收了三個引數:IApplicationBuilder型別的引數用於構建整個應用程式的配置資訊,IHostingEnvironment類的env引數用於訪問系統環境變數相關的內容,ILoggerFactory型別的loggerfactory用於日誌相關的內容處理,其中IApplicationBuilder型別的引數最為重要,該引數例項app上有一系列的擴充套件方法用於將各種Middleware註冊到request請求管線(Pipeline)中。這種方式和之前ASP.NET中的HTTP管線的主要區別是:新版本中的組合模型替換了舊版本中的事件模型。這也就要求,在新版ASP.NET中,Middleware元件註冊的順序是非常重要的,因為後一個元件可能要使用到前一個元件,所以必須按照依賴的先後順序進行註冊,舉例如下,當前MVC專案的模板程式碼示例如下:

// Add static files to the request pipeline.
app.UseStaticFiles();

// Add cookie-based authentication to the request pipeline.
app.UseIdentity();

// Add MVC to the request pipeline.
app.UseMvc(routes =>{ /*...*/});

示例中的UseStaticFilesUseIdentityUseMvc都是IApplicationBuilder上的擴充套件方法,在擴充套件方法中,都會通過呼叫擴充套件方法app.UseMiddleware方法,最終再呼叫app.Use方法來註冊新的Middleware,該方法定義如下:

public interface IApplicationBuilder
{
    //...
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}

通過程式碼,可以看出,middleware是Func<RequestDelegate, RequestDelegate>的一個例項,該Func接收一個RequestDelegate的引數,並返回一個RequestDelegate型別的值。RequestDelegate的原始碼如下:

public delegate Task RequestDelegate(HttpContext context);

通過原始碼,我們可以看出,RequestDelegate是一個委託函式,其接收HttpContext型別的例項,並返回一個Task型別的非同步物件。也就是說RequestDelegate是一個可以返回自身RequestDelegate型別函式的函式,整個ASP.NET也就是利用這種方式構建了管線(Pipelien)的組成,在這裡,每個middleware都鏈式到下一個middleware上,並在整個過程中可以對HttpConext物件進行修改或維護,當然,HttpContext中就包括了我們常操作的HttpRequestHttpResponse例項物件。

注意:HttpContextHttpRequestHttpResponse在ASP.NET 5中都是重新定義的新型別。

Middleware的定義

既然每個middleare都是Func<RequestDelegate, RequestDelegate>的一個例項,那是不是Middleware的定義要滿足一個規則?是繼承於一個抽象基類還是藉口?通過翻查相關的程式碼,我們看到,Middleware是基於約定的形式來定義的,具體約定規則如下:

  1. 建構函式的第一個引數必須是處理管線中的下一個處理函式,即RequestDelegate;
  2. 必須有一個 Invoke 函式, 並接受上下文引數(即HttpContent), 然後返回 Task;

示例如下:

public class MiddlewareName
{
    RequestDelegate _next;

    public MiddlewareName(RequestDelegate next)
    {
        _next = next;// 接收傳入的RequestDelegate例項
    }

    public async Task Invoke(HttpContext context)
    {
        // 處理程式碼,如處理context.Request中的內容

        Console.WriteLine("Middleware開始處理");

        await _next(context);

        Console.WriteLine("Middleware結束處理");

        // 處理程式碼,如處理context.Response中的內容
    }
}

通過該模板程式碼可以看到,首先一個Middleware的建構函式要接收一個RequestDelegate的例項,先儲存在一個私有變數裡,然後通過呼叫Invoke方法(並接收HttpContent例項)並返回一個Task,並且在呼叫Invoke的方法中,要通過await _next(context);語句,鏈式到下一個Middleware上,我們的處理程式碼主要就是在鏈式語句的前後執行相關的程式碼。

舉個例子,如果我們要想記錄頁面的執行時間,首先,我們先定義一個TimeRecorderMiddleware,程式碼如下:

public class TimeRecorderMiddleware
{
    RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        var sw = new Stopwatch();
        sw.Start();


        await _next(context);

        var newDiv = @"<div id=""process"">頁面處理時間:{0} 毫秒</div></body>";
        var text = string.Format(newDiv, sw.ElapsedMilliseconds);
        await context.Response.WriteAsync(text);
    }
}

Middleware的註冊有很多種方式,如下是例項型註冊程式碼:

app.Use(next => new TimeRecorderMiddleware(next).Invoke);

或者,你也可以使用UseMiddleware擴充套件方法進行註冊,示例如下:

app.UseMiddleware<TimeRecorderMiddleware>();
//app.UseMiddleware(typeof(TimeRecorderMiddleware)); 兩種方式都可以

當然,你也可以定義一個自己的擴充套件方法用於註冊該Middleware,程式碼如下:

public static IApplicationBuilder UseTimeRecorderMiddleware(this IApplicationBuilder app)
{
    return app.UseMiddleware<TimeRecorderMiddleware>();
}

最後在Startup類的Configure方法內進行註冊,程式碼如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
    app.UseTimeRecorderMiddleware(); // 要放在前面,以便進行統計,如果放在Mvc後面的話,就統計不了時間了。

    // 等等
}

編譯,重啟,並訪問頁面,在頁面的底部即可看到頁面的執行時間提示內容。

常用Middleware功能的使用

app.UseErrorPage()
在IHostingEnvironment.EnvironmentName為Development的情況下,才顯示錯誤資訊,並且錯誤資訊的顯示種類,可以通過額外的ErrorPageOptions引數來設定,可以設定全部顯示,也可以設定只顯示Cookies、Environment、ExceptionDetails、Headers、Query、SourceCode SourceCodeLineCount中的一種或多種。

app.UseErrorHandler("/Home/Error")
捕獲所有的程式異常錯誤,並將請求跳轉至指定的頁面,以達到友好提示的目的。

app.UseStaticFiles()
開啟靜態檔案也能走該Pipeline管線處理流程的功能。

app.UseIdentity()
開啟以cookie為基礎的ASP.NET identity認證功能,以支援Pipeline請求處理。

直接使用委託定義Middleware的功能

由於Middleware是Func<RequestDelegate, RequestDelegate>委託型別的例項,所以我們也可以不必定義一個單獨的類,在Startup類裡,使用委託呼叫的方式就可以了,示例如下:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
   app.Use(new Func<RequestDelegate, RequestDelegate>(next => content => Invoke(next, content)));
   // 其它
}

// 注意Invoke方法的引數
private async Task Invoke(RequestDelegate next, HttpContext content)
{
   Console.WriteLine("初始化元件開始");
   await next.Invoke(content);
   Console.WriteLine("管道下步執行完畢");
}

做個簡便的Middleware基類

雖然有約定方法,但有時候我們在開發的時候往往會犯迷糊,想不起來到底是什麼樣的約定,所以,在這裡我們可以定義一個抽象基類,然後以後所有的Middleware在定義的時候都繼承該抽象類並重載Invoke方法即可,從而可以避免約定忘記的問題。程式碼如下:

/// <summary>
/// 抽象基類
/// </summary>
public abstract class AbstractMiddleware
{
    protected RequestDelegate Next { get; set; }
    protected AbstractMiddleware(RequestDelegate next)
    {
        this.Next = next;
    }
    public abstract Task Invoke(HttpContext context);
}

/// <summary>
/// 示例Middleware
/// </summary>
public class DemoMiddleware : AbstractMiddleware
{
    public DemoMiddleware(RequestDelegate next) : base(next)
    {
    }
    public async override Task Invoke(HttpContext context)
    {
        Console.WriteLine("DemoMiddleware Start.");
        await Next.Invoke(context);
        Console.WriteLine("DemoMiddleware End.");
    }
}

使用方法和上面的一樣。

終止鏈式呼叫或阻止所有的Middleware

在有些情況下,當然根據某些條件判斷以後,可能不在需要繼續往下執行下去了,而是想知己誒返回結果,那麼你可以在你的Middleware裡忽略對await next.Invoke(content);的呼叫,直接使用·Response.WriteAsync·方法輸出內容。

另外,在有些情況下,你可能需要實現類似之前版本中的handler的功能,即不經常任何Pipeline直接對Response進行響應,新版ASP.NET裡提供了一個run方法用於實現該功能,只需要在Configure方法裡呼叫如下程式碼即可實現類似的內容輸出。

app.Run(async context =>
{
    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync("Hello World!");
});

關於ASP.NET 5 Runtime的內容,請訪問:https://msdn.microsoft.com/en-us/magazine/dn913182.aspx

遺留問題

在Mvc專案中,所有的依賴注入型別都是通過IServiceProvider例項來獲取的,目前可以通過以下形式獲取該例項:

var services = Context.RequestServices; // Controller中
var services = app.ApplicationServices; // Startup中

獲取了該例項以後,即可通過如下方法來獲取某個型別的物件:

var controller = (AccountController)services.GetService(typeof(AccountController));
// 要判斷獲取到的物件是否為null

如果你引用了Microsoft.Framework.DependencyInjection名稱空間的話,還可以使用如下三種擴充套件方法:

var controller2 = (AccountController)services.GetService<AccountController>();  
// 要判斷獲取到的物件是否為null

//如下兩種方式,如果獲取到的AccountController例項為null的話,就會欄位拋異常,而不是返回null
var controller3 = (AccountController)services.GetRequiredService(typeof(AccountController));
var controller4 = (AccountController)services.GetRequiredService<AccountController>();

那麼問題來了?如何不在Startup和Controller裡就可以獲取到HttpContext和IApplicationBuilder例項以便使用這些依賴注入服務?

  1. 如何獲取IApplicationBuilder例項?
    答案:在Startup裡將IApplicationBuilder例項儲存在一個單例中的變數上,後期全站就可以使用了。

  2. 如何獲取HttpContext例項?
    答案:參考依賴注入章節的普通類的依賴注入

引用:http://www.mikesdotnetting.com/article/269/asp-net-5-middleware-or-where-has-my-httpmodule-gone

同步與推薦