在 ASP.NET 中,我們知道,它有一個面向切麵的請求管道,有19個主要的事件構成,能夠讓我們進行靈活的擴展。通常是在 web.config 中通過註冊 HttpModule 來實現對請求管道事件監聽,並通過 HttpHandler 進入到我們的應用程式中。而在 ASP.NET Core 中,對請 ...
在 ASP.NET 中,我們知道,它有一個面向切麵的請求管道,有19個主要的事件構成,能夠讓我們進行靈活的擴展。通常是在 web.config 中通過註冊 HttpModule 來實現對請求管道事件監聽,並通過 HttpHandler 進入到我們的應用程式中。而在 ASP.NET Core 中,對請求管道進行了重新設計,通過使用一種稱為中間件的方式來進行管道的註冊,同時也變得更加簡潔和強大。
目錄
本系列文章將會從源碼分析來講解 ASP.NET Core 的運行原理,分為以下幾個章節:
ASP.NET Core 運行原理解剖[1]:Hosting
ASP.NET Core 運行原理解剖[2]:Hosting補充之配置介紹
ASP.NET Core 運行原理解剖[3]:Middleware-請求管道的構成(Current)
ASP.NET Core 運行原理解剖[4]:進入HttpContext的世界(待續)
ASP.NET Core 運行原理解剖[5]:Authentication(待續)
IApplicationBuilder
在第一章中,我們就介紹過 IApplicationBuilder
,在我們熟悉的 Startup 類的Configure
方法中,通常第一個參數便是IApplicationBuilder
,對它應該是非常熟悉了,而在這裡,就再徹底的解剖一下 IApplicationBuilder 對象。
首先,IApplicationBuilder 是用來構建請求管道的,而所謂請求管道,本質上就是對 HttpContext 的一系列操作,即通過對 Request 的處理,來生成 Reponse。因此,在 ASP.NET Core 中定義了一個 RequestDelegate 委托,來表示請求管道中的一個步驟,它有如下定義:
public delegate Task RequestDelegate(HttpContext context);
而對請求管道的註冊是通過 Func<RequestDelegate, RequestDelegate>
類型的委托(也就是中間件)來實現的。
為什麼要設計一個這樣的委托呢?讓我們來分析一下,它接收一個 RequestDelegate 類型的參數,並返回一個 RequestDelegate 類型,也就是說前一個中間件的輸出會成為下一個中間件的輸入,這樣把他們串聯起來,形成了一個完整的管道。那麼第一個中間件的輸入是什麼,最後一個中間件的輸出又是如何處理的呢?帶著這個疑惑,我們慢慢往下看。
IApplicationBuilder 的預設實現是 ApplicationBuilder,它的定義在 HttpAbstractions 項目中 :
public interface IApplicationBuilder
{
IServiceProvider ApplicationServices { get; set; }
IFeatureCollection ServerFeatures { get; }
IDictionary<string, object> Properties { get; }
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
IApplicationBuilder New();
RequestDelegate Build();
}
public class ApplicationBuilder : IApplicationBuilder
{
private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
...
}
它有一個內部的 Func<RequestDelegate, RequestDelegate>
類型的集合(用來保存我們註冊的中間件)和三個核心方法:
Use
Use
是我們非常熟悉的註冊中間件的方法,其實現非常簡單,就是將註冊的中間件保存到其內部屬性 _components
中。
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
return this;
}
我們使用Use註冊兩個簡單的中間件:
public void Configure(IApplicationBuilder app)
{
app.Use(next =>
{
Console.WriteLine("A");
return async (context) =>
{
// 1. 對Request做一些處理
// TODO
// 2. 調用下一個中間件
Console.WriteLine("A-BeginNext");
await next(context);
Console.WriteLine("A-EndNext");
// 3. 生成 Response
//TODO
};
});
app.Use(next =>
{
Console.WriteLine("B");
return async (context) =>
{
// 1. 對Request做一些處理
// TODO
// 2. 調用下一個中間件
Console.WriteLine("B-BeginNext");
await next(context);
Console.WriteLine("B-EndNext");
// 3. 生成 Response
//TODO
};
});
}
如上,註冊了A和B兩個中間件,通常每一個中間件有如上所示三個處理步驟,也就是圍繞著Next
分別對Request和Respone做出相應的處理,而B的執行會嵌套在A的裡面,因此A是第一個處理Request,並且最後一個收到Respone,這樣就構成一個經典的的U型管道。
而上面所示代碼的執行結算如下:
非常符合我們的預期,但是最終返回的結果是一個 404 HttpNotFound
,這又是為什麼呢?讓我們再看一下它的 Build
方法。
Build
第一章中,我們介紹到,在 Hosting 的啟動中,便是通過該 Build
方法創建一個 RequestDelegate 類型的委托,Http Server 通過該委托來完成整個請求的響應,它有如下定義:
public RequestDelegate Build()
{
RequestDelegate app = context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
};
foreach (var component in _components.Reverse())
{
app = component(app);
}
return app;
}
可以看到首先定義了一個 404
的中間件,然後使用了Reverse
函數將註冊的中間件列表進行反轉,因此首先執行我們所註冊的最後一個中間件,輸入參數便是一個 404
,依次執行到第一個中間件,將它的輸出傳遞給 HostingApplication
再由 IServer
來執行。整個構建過程是類似於俄羅斯套娃,按我們的註冊順序從裡到外,一層套一層。
最後,再解釋一下,上面的代碼返回404
的原因。RequestDelegate的執行是從俄羅斯套娃的最外層開始,也就是從我們註冊的第一個中間件A開始執行,A調用B,B則調用前面介紹的404
的中間件,最終也就返回了一個 404
,那如何避免返回404
呢,這時候就要用到 IApplicationBuilder 的擴展方法Run
了。
Run
對於上面 404
的問題,我們只需要對中間件A做如下修改即可:
app.Use(next =>
{
Console.WriteLine("B");
return async (context) =>
{
// 1. 對Request做一些處理
// TODO
// 2. 調用下一個中間件
Console.WriteLine("B-BeginNext");
await context.Response.WriteAsync("Hello ASP.NET Core!");
Console.WriteLine("B-EndNext");
// 3. 生成 Response
//TODO
};
});
將之前的 await next(context);
替換成了 await context.Response.WriteAsync("Hello ASP.NET Core!");
,自然也就將404
替換成了返回一個 "Hello ASP.NET Core!"
字元串。
在我們註冊的中間件中,是通過 Next
委托 來串連起來的,如果在某一個中間件中沒有調用 Next
委托,則該中間件將做為管道的終點,因此,我們在最後一個中間件不應該再調用 Next
委托,而 Run
擴展方法,通常用來註冊最後一個中間件,有如下定義:
public static class RunExtensions
{
public static void Run(this IApplicationBuilder app, RequestDelegate handler)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
}
app.Use(_ => handler);
}
}
可以看到,Run
方法接收的只有一個 RequestDelegate
委托,沒有了 Next
委托,進而保證了它不會再調用下一個中間件,即使我們在它之後註冊了其它中間件,也不會被執行。因此建議,我們最終處理 Response 的中間件使用 Run
來註冊,類似於 ASP.NET 4.x 中的 HttpHandler
。
New
而 IApplicationBuilder 還有一個常用的 New
方法,通常用來創建分支:
public class ApplicationBuilder : IApplicationBuilder
{
private ApplicationBuilder(ApplicationBuilder builder)
{
Properties = new CopyOnWriteDictionary<string, object>(builder.Properties, StringComparer.Ordinal);
}
public IApplicationBuilder New()
{
return new ApplicationBuilder(this);
}
}
New 方法根據自身來“克隆”了一個新的 ApplicationBuilder 對象,而新的 ApplicationBuilder 可以訪問到創建它的對象的 Properties
屬性,但是對自身 Properties
屬性的修改,卻不到影響到它的創建者,這是通過 CopyOnWriteDictionary
來實現的:
internal class CopyOnWriteDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
private readonly IDictionary<TKey, TValue> _sourceDictionary;
public CopyOnWriteDictionary(IDictionary<TKey, TValue> sourceDictionary, IEqualityComparer<TKey> comparer)
{
_sourceDictionary = sourceDictionary;
_comparer = comparer;
}
private IDictionary<TKey, TValue> ReadDictionary => _innerDictionary ?? _sourceDictionary;
private IDictionary<TKey, TValue> WriteDictionary =>
{
if (_innerDictionary == null)
{
_innerDictionary = new Dictionary<TKey, TValue>(_sourceDictionary, _comparer);
}
return _innerDictionary;
};
}
最後再放一張網上經典的 ASP.NET Core 請求管道圖:
IMiddleware
通過上面的介紹,我們知道,中間件本質上就是一個類型為 Func<RequestDelegate, RequestDelegate>
的委托對象,但是直接使用這個委托對象還是多有不便,因此 ASP.NET Core 提供了一個更加具體的中間件的概念,我們在大部分情況下都會將中間件定義成一個單獨的類型,使代碼更加清晰。
首先看一下 IMiddleware
介面定義:
public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}
IMiddleware 中只有一個方法:InvokeAsync
,它接收一個 HttpContext
參數,用來處理HTTP請求,和一個 RequestDelegate
參數,代表下一個中間件。當然, ASP.NET Core 並沒有要求我們必須實現 IMiddleware
介面,我們也可以像 Startup
類的實現方式一樣,通過遵循一些約定來更加靈活的定義我們的中間件。
UseMiddleware
對於 IMiddleware 類型的中間件的註冊,使用 UseMiddleware
擴展方法,定義如下:
public static class UseMiddlewareExtensions
{
public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
{
return app.UseMiddleware(typeof(TMiddleware), args);
}
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
return UseMiddlewareInterface(app, middleware);
}
...
}
}
泛型的註冊方法,在 ASP.NET Core 中比較常見,比如日誌,依賴註入中都有類似的方法,它只是一種簡寫形式,最終都是將泛型轉換為Type
類型進行註冊。
如上代碼,首先通過通過 IsAssignableFrom
方法來判斷是否實現 IMiddleware
介面,從而分為了兩種方式實現方式,我們先看一下實現了 IMiddleware
介面的中間件的執行過程:
private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, Type middlewareType)
{
return app.Use(next =>
{
return async context =>
{
var middlewareFactory = (IMiddlewareFactory)context.RequestServices.GetService(typeof(IMiddlewareFactory));
var middleware = middlewareFactory.Create(middlewareType);
try
{
await middleware.InvokeAsync(context, next);
}
finally
{
middlewareFactory.Release(middleware);
}
};
});
}
如上,創建了一個 Func<RequestDelegate, RequestDelegate>
委托,在返回的 RequestDelegate
委托中調用我們的 IMiddleware 中間件的 InvokeAsync
方法。其實也只是簡單的對 Use
方法的一種封裝。而 IMiddleware 實例的創建則使用 IMiddlewareFactory 來實現的:
public class MiddlewareFactory : IMiddlewareFactory
{
private readonly IServiceProvider _serviceProvider;
public MiddlewareFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMiddleware Create(Type middlewareType)
{
return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
}
public void Release(IMiddleware middleware)
{
}
}
通過如上代碼,可以發現一個坑,因為 IMiddleware 實例的創建是直接從 DI 容器中來獲取的,也就是說,如果我們沒有將我們實現了 IMiddleware
介面的中間件註冊到DI中,而直接使用 UseMiddleware
來註冊時,會報錯:“`InvalidOperationException: No service for type 'MiddlewareXX' has been registered.”。
不過通常我們並不會去實現 IMiddleware 介面,而是採用基於約定的,更加靈活的方式來定義中間件,而此時,UseMiddleware
方法會通過反射來創建中間件的實例:
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
// 未實例 IMiddleware 時的註冊方式
return app.Use(next =>
{
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
var invokeMethods = methods.Where(m =>
string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
|| string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
).ToArray();
...
var methodinfo = invokeMethods[0];
var parameters = methodinfo.GetParameters();
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
if (parameters.Length == 1)
{
return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance);
}
var factory = Compile<object>(methodinfo, parameters);
return context =>
{
return factory(instance, context, serviceProvider);
};
});
}
首先是根據命名約定來判斷我們的註冊的 Middleware 類是否符合要求,然後使用ActivatorUtilities.CreateInstance
調用構造函數,創建實例。而在調用構造函數時需要的碼數,會先在傳入到 UseMiddleware
方法中的參數 args
中來查找 ,如果找不到則再去DI中查找,再找不到,將會拋出一個異常。實例創建成功後,調用Invoke/InvokeAsync
方法,不過針對Invoke方法的調用並沒有直接使用反射來實現,而是採用表了達式,後者具有更好的性能,感興趣的可以去看完整代碼 UseMiddlewareExtensions 中的 Compile
方法。
通過以上代碼,我們也可以看出 IMiddleware
的命名約定:
必須要有一個 Invoke 或 InvokeAsync 方法,兩者也只能存在一個。
返回類型必須是 Task 或者繼承自 Task。
Invoke 或 InvokeAsync 方法必須要有一個 HttpContext 類型的參數。
不過,需要註意的是,Next
委托必須放在構造函數中,而不能放在 InvokeAsync
方法參數中,這是因為 Next
並不在DI系統中,而 ActivatorUtilities.CreateInstance
創建實例時,也會檢查構造中是否具有 RequestDelegate
類型的 Next
參數,如果沒有,則會拋出一個異常:“A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.”。
UseWhen
在有些場景下,我們可能需要針對某些請求,做一些特定的操作。當然,我們可以定義一個中間件,在中間件中判斷該請求是否符合我們的預期,進而選擇是否執行該操作。但是有一種更好的方式 UseWhen 來實現這樣的需求。從名字我們可以猜出,它提供了一種基於條件來註冊中間件的方式,有如下定義:
using Predicate = Func<HttpContext, bool>;
public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration)
{
var branchBuilder = app.New();
configuration(branchBuilder);
return app.Use(main =>
{
branchBuilder.Run(main);
var branch = branchBuilder.Build();
return context =>
{
if (predicate(context))
{
return branch(context);
}
else
{
return main(context);
}
};
});
}
首先使用上面介紹過的 New
方法創建一個管道分支,將我們傳入的 configuration
委托註冊到該分支中,然後再將 Main
也就是後續的中間件也註冊到該分支中,最後通過我們指定的 Predicate
來判斷是執行新分支,還是繼續在之前的管道中執行。
它的使用方式如下:
public void Configure(IApplicationBuilder app)
{
app.UseMiddlewareA();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.UseMiddlewareB();
});
app.UseMiddlewareC);
}
我們註冊了三個中間件:A, B, C 。中間件 A 和 C 會一直執行(除了短路的情況), 而 B 只有在符合預期時,也就是當請求路徑以 /api
開頭時,才會執行。
UseWhen是非常強大和有用的,建議當我們想要針對某些請求做一些特定的處理時,我們應該只為這些請求註冊特定的中間件,而不是在中間件中去判斷請求是否符合預期來選擇執行某些操作,這樣能有更好的性能。
以下是 UseWhen
的一些使用場景:
- 分別對MVC和WebAPI做出不同的錯誤響應。
- 為特定的IP添加診斷響應頭。
- 只對匿名用戶使用輸出緩存。
- 針對某些請求進行統計。
MapWhen
MapWhen 與 UseWhen 非常相似,但是他們有著本質的區別,先看一下 MapWhen
的定義:
using Predicate = Func<HttpContext, bool>;
public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action<IApplicationBuilder> configuration)
{
var branchBuilder = app.New();
configuration(branchBuilder);
var branch = branchBuilder.Build();
// put middleware in pipeline
var options = new MapWhenOptions
{
Predicate = predicate,
Branch = branch,
};
return app.Use(next => new MapWhenMiddleware(next, options).Invoke);
}
如上,可以看出他們的區別:MapWhen
並沒有將父分支中的後續中間件註冊進來,而是一個獨立的分支,而在 MapWhenMiddleware
中只是簡單的判斷是執行新分支還是舊分支:
public class MapWhenMiddleware
{
...
public async Task Invoke(HttpContext context)
{
if (_options.Predicate(context))
{
await _options.Branch(context);
}
else
{
await _next(context);
}
}
}
再看一下 MapWhen
的運行效果:
public void Configure(IApplicationBuilder app)
{
app.UseMiddlewareA();
app.MapWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.UseMiddlewareB();
});
app.UseMiddlewareC();
}
如上,中間件A將一直執行,之後如果請求路徑以 /api
開頭,則會執行 B ,併到此結束,不會再執行 C ,反之,不執行 B ,而執行 C 以及後續的其它的中間件。
當我們希望某些請求使用完全獨立的處理方式時,MapWhen
就非常有用,如 UseStaticFiles
:
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Path.Value.StartsWithSegments("/assets"),
appBuilder => appBuilder.UseStaticFiles());
}
如上,只有以 /assets
開頭的請求,才會執行 StaticFiles
中間件,而其它請求則不會執行 StaticFiles
中間件,這樣可以帶來稍微的性能提升。
UsePathBase
UsePathBase用於拆分請求路徑,類似於 MVC 中 Area
的效果,它不會創建請求管道分支,不影響管道的流程,僅僅是設置 Request 的 Path
和 PathBase
屬性:
public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase)
{
pathBase = pathBase.Value?.TrimEnd('/');
if (!pathBase.HasValue)
{
return app;
}
return app.UseMiddleware<UsePathBaseMiddleware>(pathBase);
}
public class UsePathBaseMiddleware
{
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.StartsWithSegments(_pathBase, out matchedPath, out remainingPath))
{
var originalPath = context.Request.Path;
var originalPathBase = context.Request.PathBase;
context.Request.Path = remainingPath;
context.Request.PathBase = originalPathBase.Add(matchedPath);
try
{
await _next(context);
}
finally
{
context.Request.Path = originalPath;
context.Request.PathBase = originalPathBase;
}
}
else
{
await _next(context);
}
}
}
如上,當請求路徑以我們指定的 PathString
開頭時,則將請求的 PathBase 設置為 傳入的 pathBase
,Path 則為剩下的部分。
PathString 用來表示請求路徑的一個片段,它可以從字元串隱式轉換,但是要求必須以
/
開頭,並且不以/
結尾。
Map
Map 包含 UsePathBase
的功能,並且創建一個獨立的分支來完成請求的處理,類似於 MapWhen
:
public static class MapExtensions
{
public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)
{
...
return app.Use(next => new MapMiddleware(next, options).Invoke);
}
}
以上方法中與 MapWhen
一樣,不同的只是 Map
調用了 MapMiddleware 中間件:
public class MapMiddleware
{
...
public async Task Invoke(HttpContext context)
{
PathString matchedPath;
PathString remainingPath;
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matchedPath, out remainingPath))
{
var path = context.Request.Path;
var pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matchedPath);
context.Request.Path = remainingPath;
try
{
await _options.Branch(context);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
}
else
{
await _next(context);
}
}
}
如上,可以看出 Map
擴展方法比 MapWhen
多了對 Request.PathBase
和 Request.Path
的處理,最後演示一下 Map
的用例:
public void Configure(IApplicationBuilder app)
{
app.Map("/account", builder =>
{
builder.Run(async context =>
{
Console.WriteLine($"PathBase: {context.Request.PathBase}, Path: {context.Request.Path}");
await context.Response.WriteAsync("This is from account");
});
});
app.Run(async context =>
{
Console.WriteLine($"PathBase: {context.Request.PathBase}, Path: {context.Request.Path}");
await context.Response.WriteAsync("This is default");
});
}
如上,我們為 /account
定義了一個分支,當我們 /account/user
的時候,將返回 This is from account
,並且會將 Request.PathBase 設置為 /account
,將 Request.Path 設置為 /user
。
總結
本文詳細介紹了 ASP.NET Core 請求管道的構建過程,以及一些幫助我們更加方便的來配置請求管道的擴展方法。在 ASP.NET Core 中,至少要有一個中間件來響應請求,而我們的應用程式實際上只是中間件的集合,MVC 也只是其中的一個中間件而已。簡單來說,中間件就是一個處理http請求和響應的組件,多個中間件構成了請求處理管道,每個中間件都可以選擇處理結束,還是繼續傳遞給管道中的下一個中間件,以此串聯形成請求管道。通常,我們註冊的每個中間件,每次請求和響應均會被調用,但也可以使用 Map
, MapWhen
,UseWhen
等擴展方法對中間件進行過濾。
參考資料: