程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 學習ASP.NET Core,怎能不了解請求處理管道[1]: 中間件究竟是個什麼東西?,asp.netcore

學習ASP.NET Core,怎能不了解請求處理管道[1]: 中間件究竟是個什麼東西?,asp.netcore

編輯:關於.NET

學習ASP.NET Core,怎能不了解請求處理管道[1]: 中間件究竟是個什麼東西?,asp.netcore


ASP.NET Core管道雖然在結構組成上顯得非常簡單,但是在具體實現上卻涉及到太多的對象,所以我們在 “通過重建Hosting系統理解HTTP請求在ASP.NET Core管道中的處理流程”(上篇、中篇、下篇) 中圍繞著一個經過極度簡化的模擬管道講述了真實管道構建的方式以及處理HTTP請求的流程。在本系列 中,我們會還原構建模擬管道時可以捨棄和改寫的部分,向讀者朋友們呈現一個真是的HTTP請求處理管道。 ASP.NET Core 的請求處理管道由一個服務器與一組有序排列的中間件構成,前者僅僅完成請求監聽、接收和響應這些與底層網絡相關的工作,至於請求接收之後和響應之前的所有工作都交給中間件來完成。ASP.NET Core的中間件通過一個類型Func<RequestDelegate, RequestDelegate>的委托對象來表示,而RequestDelegate也是一個委托,它代表一項請求處理任務。 [本文已經同步到《ASP.NET Core框架揭秘》之中]

 

目錄
一、RequestDelegate
二、HttpContext
    FeatureCollection
    DefaultHttpContext
    HttpContextFactory
三、ApplicationBuilder
    ApplicationBuilderFactory
    中間件類型
    中間件類型的注冊

一、RequestDelegate

服務器接受到抵達的HTTP請求之後會構建一個描述當前請求的原始上下文,服務器的類型決定了這個原始上下文的類型,比如在我們模擬管道默認采用的HttpListenerServer由於采用HttpListener來監聽、接收並響應請求,所以它對應的原始上下文是一個HttpListenerContext對象。但是對於管道的後續部分,即由注冊的中間件構建的鏈表,它們需要采用統一的方式來處理請求,所以服務器最終會根據原始的上下文來創建一個抽象的HTTP上下文,後者通過抽象類HttpContext來表示。

我們不僅可以利用這個HttpContext獲取描述當前請求的上下文信息,同樣可以利用它來實現對響應的控制。針對當前請求的任何處理操作總是在這麼一個上下文中進行,所以一項請求處理任務完全可以抽象成一個類型Func<HttpContext,Task>的委托來表示,實際上具有如下定義的RequestDelegate委托具有類似的定義。

   1: public delegate Task RequestDelegate(HttpContext context); 

每個中間件都承載著獨立的請求處理任務,它本質上也體現了在當前HttpContext下針對請求的處理操作,那麼為什麼中間件不直接通過一個RequestDelegate對象來表示,而是表示為一個類型為Func<RequestDelegate, RequestDelegate>的委托對象呢?原因很簡單,中間件並不孤立地存在,所有注冊的中間件最終會根據注冊的先後順序組成一個鏈表,每個中間件不僅僅需要完成各自的請求處理任務外,還需要驅動鏈表中的下一個中間件。

如上圖所示,對於一個由多個Func<RequestDelegate, RequestDelegate>對象組成的中間鏈表來說,某個中間件會將後一個Func<RequestDelegate, RequestDelegate>對象的返回值作為輸入,而自身的返回值則作為前一個中間件的輸入。某個中間件執行之後返回的RequestDelegate對象不僅僅體現了自身對請求的處理操作,而是體現了包含自己和後續中間件一次對請求的處理。那麼對於第一個中間件來說,它執行後返回的RequestDelegate對象實際上體現了整個應用對請求的處理邏輯。

二、 HttpContext

對當前上下文的抽象解除了管道對具體服務器類型的依賴, 這使我們可以為ASP.NET Core應用自由地選擇承載(Hosting)方式,而不是像傳統的ASP.NET應用一樣只能寄宿在IIS之中。抽象HTTP上下文的目的是為了實現對請求處理流程的抽象,只有這樣我們才能將針對請求的某項操作體現在一個標准的中間件上,有了這個這個標准化的中間件才有所謂的請求處理管道。

ASP.NET Core通過具有如下所示的HttpContext類來表示這麼一個抽象的HTTP上下文。對於一個HttpContext對象來說,它的核心體現在用於描述請求和響應的Request和Response屬性之上。除此之外,我們還可以通過它獲取與當前請求相關的其他上下文信息,比如用來控制用戶認證的AuthenticationManager對象和代表當前請求用戶的ClaimsPrincipal對象,以及描述當前HTTP連接的ConnectionInfo對象和用於控制WebSocket的WebSocketManager。我們可以獲取並控制當前會話,也可以獲取或者設置調試追蹤的ID。

   1: public abstract class HttpContext
   2: {
   3:  
   4:     public abstract HttpRequest     Request { get; }
   5:     public abstract HttpResponse    Response { get; }
   6:  
   7:     public abstract AuthenticationManager            Authentication { get; }
   8:     public abstract ClaimsPrincipal                  User { get; set; }
   9:     public abstract ConnectionInfo                   Connection { get; } 
  10:     public abstract WebSocketManager                 WebSockets { get; } 
  11:     public abstract ISession                         Session { get; set; } 
  12:     public abstract string                           TraceIdentifier { get; set; }
  13:     public abstract CancellationToken                RequestAborted { get; set; }  
  14:     public abstract IDictionary<object, object>      Items { get; set; }  
  15:  
  16:     public abstract IServiceProvider                RequestServices { get; set; }
  17:     public abstract IFeatureCollection              Features { get; }
  18: }

當需要中指對請求的處理時,我們可以通過為RequestAborted屬性設置一個CancellationToken對象從而將終止通知發送給管道。如果需要對整個管道共享一些與當前上下文相關的數據,我們可以將它保存在通過Items屬性表示的字典中。我們一再提到依賴注入被廣泛地應用ASP.NET Core管道中,HttpContext的RequestServices屬性返回的根據在應用啟動時注冊的服務而創建的ServiceProvider。只要相應的服務被預先注冊到指定的服務接口上,我們就可能利用這個ServiceProvider根據這個接口得到對應的服務對象。

   1: public abstract class HttpRequest
   2: {
   3:     public abstract HttpContext                    HttpContext { get; }
   4:     public abstract string                         Method { get; set; }
   5:     public abstract string                         Scheme { get; set; }
   6:     public abstract bool                           IsHttps { get; set; }
   7:     public abstract HostString                     Host { get; set; }
   8:     public abstract PathString                     PathBase { get; set; }
   9:     public abstract PathString                     Path { get; set; }
  10:     public abstract QueryString                    QueryString { get; set; }
  11:     public abstract IQueryCollection               Query { get; set; }
  12:     public abstract string                         Protocol { get; set; }
  13:     public abstract IHeaderDictionary              Headers { get; } >
  14:     public abstract IRequestCookieCollection       Cookies { get; set; }
  15:     public abstract string                         ContentType { get; set; }
  16:     public abstract Stream                         Body { get; set; }
  17:     public abstract bool                           HasFormContentType { get; }
  18:     public abstract IFormCollection                Form { get; set; }
  19:  
  20:     public abstract Task<IFormCollection>         ReadFormAsync(CancellationToken cancellationToken);
  21: }

如上所示的是抽象類HttpRequest是對HTTP請求的描述,它是HttpContext的只讀屬性Request的返回類型。我們可以利用這個對象獲取到描述當前請求的各種相關信息,比如請求的協議(HTTP或者HTTPS)、HTTP方法、地址,也可以獲取代表請求的HTTP消息的首部和主體。

在了解了表示請求的抽象類HttpRequest之後,我們再來認識一個與之相對的用於描述響應HttpResponse類型。如下面的代碼片斷所示,HttpResponse依然是一個抽象類,我們可以通過定義在它之上的屬性和方法來控制對請求的響應。從原則上講,我們對請求的所做的任意類型的響應都可以利用它來說實現。當我們通過表示當前上下文的HttpContext對象得到表示響應的HttpResponse之後,我們不僅僅可以將希望的內容寫入響應消息的主體,還可以設置響應狀態碼以及添加相應的首部。

   1: public abstract class HttpResponse
   2: {
   3:     public abstract HttpContext           HttpContext { get; }
   4:     public abstract int                   StatusCode { get; set; }
   5:     public abstract IHeaderDictionary     Headers { get; }
   6:     public abstract Stream                Body { get; set; }
   7:     public abstract long?                 ContentLength { get; set; }
   8:     public abstract IResponseCookies      Cookies { get; }
   9:     public abstract bool                  HasStarted { get; }
  10:  
  11:     public abstract void OnStarting(Func<object, Task> callback, object state);
  12:     public virtual void OnStarting(Func<Task> callback);
  13:     public abstract void OnCompleted(Func<object, Task> callback, object state);
  14:     public virtual void RegisterForDispose(IDisposable disposable);
  15:     public virtual void OnCompleted(Func<Task> callback);
  16:     public virtual void Redirect(string location);
  17:     public abstract void Redirect(string location, bool permanent);
  18: }

FeatureCollection

HttpContext的另一個只讀屬性Features返回一組“特性”對象。在ASP.NET Core管道式處理設計中,特性是一個非常重要的概念,特性是實現抽象化HttpContext的途徑。具體來說,服務器在接收到請求之後會創建一個由自身類型決定的原始的上下文,管道不僅僅利用這個原始上下文來獲取與請求相關的信息,它對請求的最終響應實際上也是通過這個原始上下文來完成的。所以對一個HttpContext對象來說,由它描述的上下文信息不僅僅來源於這個原始的上下文,我們針對HttpContext所做的任何響應操作最終都需要分發給這個原始上下文來完成, 否則是不會生效的。抽象的HttpContext和原始上下文之間的“雙向綁定”究竟是如何實現的呢?

這個所謂的“雙向綁定”即使其實很簡單。當原始上下文被創建出來之後,服務器會將它封裝成一系列標准的特性對象,HttpContext正是對這些特性對象的封裝。一般來說,這些特性對象所對應的類型均實現了某個預定義的標准接口,接口中不僅僅定義相應的屬性來讀寫原始上下文中描述的信息,還定義了相應的方法來操作原始上下文。HttpContext的屬性Features返回的就是這組特性對象的集合,它的返回類型為IFeatureCollection,我們將實現了該接口的類型以及對應的對象統稱為FeatureCollection。

   1: public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
   2: {
   3:     TFeature Get<TFeature>();
   4:     void Set<TFeature>(TFeature instance);
   5:  
   6:     bool       IsReadOnly { get; }
   7:     object     this[Type key] { get; set; }
   8:     int        Revision { get; }
   9:  }

一個FeatureCollection對象本質上就是一個Key和Value分別為Type和Object類型的字段,話句話說,特性對象通過對應的接口類型注冊到HttpContext之上。我們通過調用Set方法將一個特性對象針對指定的類型(一般為特性接口)注冊到這個字典對象上,並通過Get方法根據注冊的類型獲取它。特性對象的注冊和獲取也可以利用定義的索引來完成。如果IsReadOnly屬性返回True,我們將不能注冊新的特性或者修改已經注冊的特性。 整數類型的之都屬性Revision可以視為整個FeatureCollection對象的版本,不論是采用何種方式注冊新的特性還是修改現有的特性,這個屬性的值都將改變。

具有如下定義的FeatureCollection類實現了IFeatureCollection接口,我們默認使用的FeatureCollection就是這麼一個類型的對象。FeatureCollection具有兩個構造函數重載,默認無參構造函數幫助我們創建一個空的特性集合,另一個構造函數則需要指定一個FeatureCollection對象來提供默認特性。對於采用第二個構造函數重載創建的 FeatureCollection對象來說,當我們通過指定某個特性接口類型試圖獲取對應的特性對象時,如果對應的特性沒有注冊到當前FeatureCollection對象上,而是注冊到提供默認特性的FeatureCollection對象上,後者將會提供最終的特性。

   1: public class FeatureCollection : IFeatureCollection
   2: {   
   3:     //其他成員
   4:     public FeatureCollection();
   5:     public FeatureCollection(IFeatureCollection defaults);
   6: }

對於FeatureCollection類型來說,它 的IsReadOnly總是返回False,所以它永遠是可讀可寫的。對於調用默認無參構造函數創建的FeatureCollection對象來說,它 的Revision默認返回零。如果我們通過指定另一個FeatureCollection對象為參數調用第二個構造函數來創建一個FeatureCollection對象,前者的Revision屬性值將成為後者同名屬性的默認值。不論我們采用何種形式(調用Set方法或者索引)添加一個新的特性或者改變了一個已經注冊的特性,FeatureCollection對象的Revision屬性都將自動遞增。上述的這些關於FeatureCollection的特性都體現在如下所示的代碼片段中。

   1: FeatureCollection defaults = new FeatureCollection();
   2: Debug.Assert(defaults.Revision == 0);
   3:  
   4: defaults.Set<IFoo>(new Foo());
   5: Debug.Assert(defaults.Revision == 1);
   6:  
   7: defaults[typeof(IBar)] = new Bar();
   8: Debug.Assert(defaults.Revision == 2);
   9:  
  10: FeatureCollection features = new FeatureCollection(defaults);
  11: Debug.Assert(features.Revision == 2);
  12: Debug.Assert(features.Get<IFoo>().GetType() == typeof(Foo));
  13:  
  14: features.Set<IBaz>(new Baz());
  15: Debug.Assert(features.Revision == 3);

DefaultHttpContext

ASP.NET Core默認使用的HttpContext類型為DefaultHttpContext,上面我們介紹的針對描述原始上下文“特性集合”來創建HttpContext的策略就體現在這個類型之上。DefaultHttpContext具有一個如下的構造函數,作為參數的FeatureCollection對象就是這麼一個特性集合。

   1: public class DefaultHttpContext : HttpContext
   2: {
   3:     public DefaultHttpContext(IFeatureCollection features);
   4: }

不論是組成管道的中間件還是建立在管道上的應用,在默認的情況下都是利用這個DefaultHttpContext對象來獲取當前請求的相關信息,並利用這個對象來控制最終發送的響應。但是DefaultHttpContext對象這個這個過程中僅僅是一個“代理”,針對它的調用(屬性或者方法)最終都需要轉發給由具體服務器創建的那個原始上下文,在構造函數中指定的這個FeatureCollection對象所代表的特性集合成為了這兩個上下文對象進行溝通的唯一渠道。對於定義在DefaultHttpContext中的所有屬性,它們幾乎都具有一個對應的特性,這些特性都對應著一個接口。表1列出了部分特性接口以及DefaultHttpContext對應的屬性。

表1 描述原始HTTP上下文的特性接口

接口

屬性

描述

IHttpRequestFeature

Request

獲取描述請求的基本信息。

IHttpResponseFeature

Response

控制對請求的響應。

IHttpAuthenticationFeature

AuthenticationManger/User

提供完成用戶認證的AuthenticationHandler對象和表示當前用戶的ClaimsPrincipal對象

IHttpConnectionFeature

Connection

提供描述當前HTTP連接的基本信息。

IItemsFeature

Items

提供用戶存放針對當前請求的對象容器。

IHttpRequestLifetimeFeature

RequestAborted

傳遞請求處理取消通知和中止當前請求處理。

IServiceProvidersFeature

RequestServices

提供根據服務注冊創建的ServiceProvider。

ISessionFeature

Session

提供描述當前會話的Session對象。

IHttpRequestIdentifierFeature

TraceIdentifier

為追蹤日志(Trace)提供針對當前請求的唯一標識。

IHttpWebSocketFeature

WebSockets

管理WebSocket

對於上面列出的眾多特性接口,我們在後續相關章節中都會涉及到,目前來說我們只需要了解一下兩個最重要的特性接口,即表示請求和響應的IHttpRequestFeature和IHttpResponseFeature。從下面給出的代碼片斷我們不難看出,這兩個接口的定義分別與抽象類HttpRequest和HttpResponse具有一致的定義。對於DefaultHttpContext類型來說,它的Request和Response屬性分別返回的是一個DefaultHttpRequest和DefaultHttpResponse對象。DefaultHttpRequest和DefaultHttpResponse分別繼承自HttpRequest和HttpResponse,它們分別利用這個兩個特性實現了從基類繼承下來的所有抽象成員。

   1: public interface IHttpRequestFeature
   2: {
   3:     Stream             Body { get; set; }
   4:     IHeaderDictionary  Headers { get; set; }
   5:     string             Method { get; set; }
   6:     string             Path { get; set; }
   7:     string             PathBase { get; set; }
   8:     string             Protocol { get; set; }
   9:     string             QueryString { get; set; }
  10:     string             Scheme { get; set; }
  11: }
  12:  
  13: public interface IHttpResponseFeature
  14: {
  15:     Stream             Body { get; set; }
  16:     bool               HasStarted { get; }
  17:     IHeaderDictionary  Headers { get; set; }
  18:     string             ReasonPhrase { get; set; }
  19:     int                 StatusCode { get; set; }
  20:  
  21:     void OnCompleted(Func<object, Task> callback, object state);
  22:     void OnStarting(Func<object, Task> callback, object state);
  23: }

對於實現請求監聽、接收和響應的服務器來說,它們都需要通過實現上面這些特性接口來定義針對性的特性類。如下圖所示,當成功接收到請求之後,服務器會創建相應的特性並將它們組合成一個FeatureCollection對象,最後創建出一個DefaultHttpContext對象,我們注冊的所有中間件針對這個DefaultHttpContext完成各自的請求處理工作。

HttpContextFactory

在服務器接收到抵達的請求時,它並不會直接利用原始的上下文去創建HttpContext對象,HttpContext在管道中的創建是間接地通過HttpContextFactory來完成的。 HttpContextFactory是對所有實現了IHttpContextFactory接口的所有類型及其對象的統稱。如下面的代碼片段所示,IHttpContextFactory接口除了定義創建HttpContext對象的Create方法之外,還定義了另一個方法Dispose來釋放指定的HttpContext對象。HttpContextFactory類是該接口的默認實現者,由它的Create方法創建並返回的自然是一個DefaultHttpContext對象。

   1: public interface IHttpContextFactory
   2: {
   3:     HttpContext Create(IFeatureCollection featureCollection);
   4:     void Dispose(HttpContext httpContext);
   5: }
   6:  
   7: public class HttpContextFactory : IHttpContextFactory
   8: {    
   9:     //省略其他成員
  10:     public HttpContext Create(IFeatureCollection featureCollection);
  11:     public void Dispose(HttpContext httpContext);
  12: }

綜上所述,組成管道的所有中間件在一個標准化的上下文中完整對請求的處理,這個上下文通過抽象類HttpContext表示,ASP.NET Core默認使用的是它的子類DefaultHttpContext。一個DefaultHttpContext對象是根據描述原始上下文的特性集合,每個特性對應的類型都實現了標准的接口,接口IHttpRequestFeature和IHttpResponseFeature分別代表針對請求和響應的特性。HttpContext默認情況下是通過注冊的工廠創建的,該工廠通過接口IHttpContextFactory表示,默認使用的HttpContext工廠類型為HttpContextFactory,它也是DefaultHttpContext對象的創建者。

三、ApplicationBuilder

以類型為Func<RequestDelegate, RequestDelegate>的委托對象表示的中間件需要在啟動的時候注冊到應用程序上。所有注冊的中間件最終會轉換成一個RequestDelegate類型的委托對象,它們按照注冊順序對請求的處理流程最終體現在對這個委托對象的執行。不論是最終將中間件轉換成RequestDelegate對象,還是最初對它們的注冊,都是通過一個名為ApplicationBuilder的對象來完成的。

ApplicationBuilder是我們對所有實現了IApplicationBuilder接口的所有類型以及對應對象的統稱。接口IApplicationBuilder定義如下,中間件的注冊和RequestDelegate對象的生成分別通過調用它的Use和Build方法來完成。除了這兩個核心方法,IApplicationBuilder接口還定義了三個屬性,其中ApplicationServices返回根據最初服務注冊生成的ServiceProvider對象,而ServerFeatures屬性返回的FeatureCollection對象是描述Server的特性集合。字典類型的Properties屬性用戶存儲任意自定義的屬性,而New方法會根據自己“克隆”出一個新的ApplicationBuilder對象,這兩個ApplicationBuilder對象應用具有相同的屬性集合。

   1: public interface IApplicationBuilder
   2: {
   3:     IServiceProvider             ApplicationServices { get; set; }
   4:     IFeatureCollection           ServerFeatures { get; }
   5:     IDictionary<string, object>  Properties { get; }
   6:  
   7:     RequestDelegate         Build();
   8:     IApplicationBuilder     New();
   9:     IApplicationBuilder     Use(Func<RequestDelegate, RequestDelegate> middleware);
  10: }

具有如下定義的ApplicationBuilder類型是對IApplicationBuilder接口的默認實現。ApplicationBuilder類型利用一個List<Func<RequestDelegate, RequestDelegate>>對象來保存注冊的中間件,所以Use方法只需要將指定的中間件添加到這個列表中即可,而Build方法只需要逆序調用這些注冊的中間件對應的Func<RequestDelegate, RequestDelegate>對象就能得到我們需要的RequestDelegate對象。值得一提的是,Build方法實際上在中間件鏈條的尾部添加了一個額外的中間件,該中間件會負責將響應狀態碼設置為404,如果我們沒有注冊一個中間件對請求作最終的響應(這樣的中間件將不會試圖調用後續中間件),整個管道比較回復一個狀態碼為404的響應。

   1: public class ApplicationBuilder : IApplicationBuilder
   2: {
   3:     private readonly IList<Func<RequestDelegate, RequestDelegate>> middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
   4:  
   5:     public IDictionary<string, object> Properties { get; }
   6:  
   7:     public IServiceProvider ApplicationServices
   8:     {
   9:         get { return GetProperty<IServiceProvider>("application.Services"); }
  10:         set { SetProperty<IServiceProvider>("application.Services", value); }
  11:     }
  12:  
  13:     public IFeatureCollection ServerFeatures
  14:     {
  15:         get { return GetProperty<IFeatureCollection>("server.Features"); }
  16:     }
  17:  
  18:  
  19:     public ApplicationBuilder(IServiceProvider serviceProvider)
  20:     {
  21:         this.Properties = new Dictionary<string, object>();
  22:         ApplicationServices = serviceProvider;
  23:     }
  24:  
  25:     public ApplicationBuilder(IServiceProvider serviceProvider, object server)
  26:         : this(serviceProvider)
  27:     {
  28:         SetProperty("server.Features", server);
  29:     }
  30:  
  31:     public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
  32:     {
  33:         middlewares.Add(middleware);
  34:         return this;
  35:     }
  36:  
  37:     public IApplicationBuilder New()
  38:     {
  39:         return new ApplicationBuilder(this);
  40:     }
  41:  
  42:     public RequestDelegate Build()
  43:     {
  44:         RequestDelegate app = context =>
  45:         {
  46:             context.Response.StatusCode = 404;
  47:             return Task.FromResult(0);
  48:         };
  49:         foreach (var component in middlewares.Reverse())
  50:         {
  51:             app = component(app);
  52:         }
  53:         return app;
  54: }
  55:  
  56:     private ApplicationBuilder(ApplicationBuilder builder)
  57:     {
  58:         this.Properties = builder.Properties;
  59:     }        
  60:  
  61:     private T GetProperty<T>(string key)
  62:     {
  63:         object value;
  64:         return Properties.TryGetValue(key, out value) ? (T)value : default(T);
  65:     }
  66:  
  67:     private void SetProperty<T>(string key, T value)
  68:     {
  69:         this.Properties[key] = value;
  70:     }
  71: }

通過上面的代碼片段我們不難看到,不論是通過ApplicationServices屬性返回的ServiceProvider對象,還是通過ServerFeatures屬性返回的用於描述Server特性的FeatureCollection對象,它們實際上都保存在通過Properties屬性返回字典對象上。ApplicationBuilder具有兩個公共構造函數重載,它們具有一個公共的參數,即用來初始化ApplicationServices屬性的參數serviceProvider。

一個構造函數具有一個名為server的參數,但是這個參數並不是表示管道使用的服務器,而是承載服務器相關特性的FeatureCollection對象,不過這個參數類型被定義成Object,而不是IFeatureCollection接口。New方法直接調用私有構造函數創建出一個新的ApplicationBuilder對象,這個對象與自己的Properties屬性共享同一個字典對象,由於ApplicationServices和ServerFeatures屬性的返回值也存放在這個字典對象上,所以New方法得到的ApplicationBuilder對象與自身對象其實是完全等效的。

ApplicationBuilderFactory

ApplicationBuilderFactory是ASP.NET Core它用來創建ApplicationBuilder的工廠,它是對所有實現了接口IApplicationBuilderFactory的所有類型以及對應對象的統稱。如下面的代碼片段所示,該接口定義了唯一個方法CreateBuilder根據提供的FeatureCollection對象創建出對應的ApplicationBuilder對象,這個FeatureCollection對象正是承載與服務器相關特性的集合。ApplicationBuilderFactory類型是該接口的默認實現者,當CreateBuilder方法被調用的時候,它會直接將構造時提供ServiceProvider對象和serverFeatures參數表示的FeatureCollection對象來創建返回的ApplicationBuilder對象。

   1: public interface IApplicationBuilderFactory
   2: {
   3:     IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures);
   4: }
   5:  
   6: public class ApplicationBuilderFactory : IApplicationBuilderFactory
   7: {
   8:     private readonly IServiceProvider _serviceProvider;
   9:  
  10:     public ApplicationBuilderFactory(IServiceProvider serviceProvider)
  11:     {
  12:         this._serviceProvider = serviceProvider;
  13:     }
  14:  
  15:     public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures)
  16:     {
  17:         return new ApplicationBuilder(_serviceProvider, serverFeatures);
  18:     }
  19: }

中間件類型

雖然中間件最終體現為一個類型為 Func<RequestDelegate, RequestDelegate>的委托對象,但是我們在大部分情況下都會將中間件定義成一個單獨的類型。雖然這樣的中間件類型不要求實現某個預定義的接口或者繼承某個預定義的基類,但是卻要遵守幾個必要的約定。接下來我們直接如下這個ContentMiddleware類說說一個合法的中間件類型應該如何定義。

   1: public class ContentMiddleare
   2: {
   3:     public RequestDelegate     _next;
   4:     public byte[]         _content;
   5:     public string         _contentType;
   6:  
   7:     public ContentMiddleare(RequestDelegate next, byte[] content, string contentType)
   8:     {
   9:         _next         = next;
  10:         _content      = content;
  11:         _contentType  = contentType;
  12:     }
  13:  
  14:     public async Task Invoke(HttpContext context, ILoggerFactory loggerFactory)
  15:     {
  16:         loggerFactory.CreateLogger<ContentMiddleare>().LogInformation($"Write content ({_contentType})");
  17:         context.Response.ContentType = _contentType;
  18:         await context.Response.Body.WriteAsync(_content,0, _content.Length);
  19:     }
  20: }

如上所示的這個中間件(ContentMiddleware)可以幫助我們將任何類型的內容響應給客戶端,它的兩個字段_content和_contentType分別代表響應內容和媒體類型(內容類型或者MIME類型),它體現了一個典型中間件類型的定義規則或者約定:

  • 應該定義成實例類,不能定義成靜態類。
  • 具有一個有效的公共構造函數。這個構造函數的第一個參數類型必須為RequestDelegate,代表對請求的後續操作(可以視為下一個注冊的中間件),至於後續參數的個數和類型則不作要求。
  • 針對請求的處理定義在一個名為Invoke的公共實例方法,其返回類型為Task。該方法的第一個參數類型為HttpContext,代表當前HTTP上下文。我們可以為這個方法定義任意數量和類型的後續參數,當這個方法被執行的時候,系統將會采用依賴注入的方式提供響應的服務來為這個參數賦值。

中間件類型的注冊

中間件類型的注冊可以通過調用 IApplicationBuilder接口的擴展方法UseMiddleware 和UseMiddleware
< TMiddleware >進行注冊。如下面的代碼片斷所示,除了指定中間件的類型之外,我們還需要按照順序指定調用目標構造函數的全部或者部分參數。不過不過的參數列表不需要提供作為第一個參數的RequestDelegate,如果僅僅指定了部分參數,缺失的參數將會自動通過ServiceProvider來提供。

   1: public static class UseMiddlewareExtensions
   2: {
   3:     public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args);
   4:     public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args);
   5: }

對於上面定義的這個 ContentMiddleare類型,我們按照如下的方式對它進行了注冊。當這個中間件執行的時候,它會響應客戶端一張PNG圖片。如果客戶端是能夠支持圖片呈現的浏覽器,這張圖片會直接顯示在浏覽器上。

   1: new WebHostBuilder()
   2:     .Configure(app=>app.UseMiddleware<ContentMiddleare>(File.ReadAllBytes("girl.png"),"image/png"))
   3: ...

雖然中間件可以定義成任何一個遵循約定的類型,但是中間件自身在ASP.NET Core框架中總是體現為一個類型為Func<RequestDelegate, RequestDelegate>的委托對象,所以上述的這個UseMiddleware方法在執行的時候需要在內部根據注冊的中間件類型和指定的參數列表創建這麼一個Func<RequestDelegate, RequestDelegate>對象。其中的邏輯並不復雜,它之需要將中間件對象的創建和針對Invoke方法的調用實現在返回的委托對象中就可以了。值得一提的是,針對Invoke方法的調用並沒有直接通過反射的方式來實現,而是采用表達式,後者具有更好的性能。在如下所示的代碼片段中,我采用最精簡的代碼模擬了UseMiddleware方法的實現。

   1: public static class WebHostBuilderExtensions
   2: {
   3:     private static MethodInfo GetServiceMethod = typeof(WebHostBuilderExtensions).GetMethod("GetService", BindingFlags.Static | BindingFlags.NonPublic);
   4:  
   5:     public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
   6:     {
   7:         return UseMiddleware2(app, typeof(TMiddleware), args);
   8:     }
   9:  
  10:     public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middlewareType, params object[] args)
  11:     {
  12:         return app.Use(next =>
  13:         {
  14:             return context => {
  15:                 var factory = Compile<object>(middlewareType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public));
  16:                 object middleware = CreateMiddleware(app, middlewareType, next, args);
  17:                 return factory(middleware, context, app.ApplicationServices);
  18:             };              
  19:         });
  20:     }
  21:  
  22:     private static object CreateMiddleware(IApplicationBuilder app, Type middlewareType, RequestDelegate next, params object[] args)
  23:     {
  24:         object[] arguments = new object[args.Length + 1];
  25:         arguments[0] = next;
  26:         args.CopyTo(arguments, 1);
  27:         return ActivatorUtilities.CreateInstance(app.ApplicationServices, middlewareType, arguments);
  28:     }
  29:     
  30:     //將對Invoke方法的調用轉換成一個Func<TMiddleware, HttpContext, IServiceProvider, Task>對象
  31:     private static Func<TMiddleware, HttpContext, IServiceProvider, Task> Compile<TMiddleware>(MethodInfo invokeMethod)
  32:     {           
  33:         ParameterExpression middleware = Expression.Parameter(typeof(TMiddleware), "middleware");
  34:         ParameterExpression httpContext = Expression.Parameter(typeof(HttpContext), "httpContext");
  35:         ParameterExpression serviceProvider = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
  36:  
  37:         var arguments = from parameter in invokeMethod.GetParameters()
  38:             select GetArgument(httpContext, serviceProvider, parameter.ParameterType);
  39:  
  40:         Expression instance = middleware;
  41:         if (invokeMethod.DeclaringType != typeof(TMiddleware))
  42:         {
  43:             instance = Expression.Convert(instance, invokeMethod.DeclaringType);
  44:         }
  45:  
  46:         Expression invoke = Expression.Call(instance, invokeMethod, arguments.ToArray());
  47:         return Expression.Lambda<Func<TMiddleware, HttpContext, IServiceProvider, Task>>(invoke, middleware, httpContext, serviceProvider).Compile();
  48:     }
  49:     
  50:     //生成調用Invoke方法的參數表達式
  51:     private static Expression GetArgument(Expression httpContext, Expression serviceProvider, Type parameterType)
  52:     {
  53:         if (parameterType == typeof(HttpContext))
  54:         {
  55:             return httpContext;
  56:         }
  57:         Expression serviceType = Expression.Constant(parameterType, typeof(Type));
  58:         Expression callGetService = Expression.Call(GetServiceMethod, serviceProvider, serviceType);
  59:         return Expression.Convert(callGetService, parameterType);
  60:     }
  61:  
  62:     private static object GetService(IServiceProvider serviceProvider, Type serviceType)
  63:     {
  64:         return serviceProvider.GetService(serviceType);
  65:     }
  66: }

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved