ASP.NET Core應用中的路由機制實現在RouterMiddleware中間件中,它的目的在於通過路由解析為請求找到一個匹配的處理器,同時將請求攜帶的數據以路由參數的形式解析出來供後續請求處理流程使用。但是具體的路由解析功能其實並沒有直接實現在RouterMiddleware中間件中,而是由一... ...
ASP.NET Core應用中的路由機制實現在RouterMiddleware中間件中,它的目的在於通過路由解析為請求找到一個匹配的處理器,同時將請求攜帶的數據以路由參數的形式解析出來供後續請求處理流程使用。但是具體的路由解析功能其實並沒有直接實現在RouterMiddleware中間件中,而是由一個Router對象來完成的。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、IRouter介面
二、RouteContext
三、RouteData
四、Route
五、RouteHandler
總結
一、IRouter介面
Router是我們對所有實現了IRouter介面的所有類型以及對應對象的統稱,如下麵所示的RouterMiddleware類型定義可以看出,當我們創建這個中間件對象的時候,我們需要指定這個Router。
1: public class RouterMiddleware
2: {
3: public RouterMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IRouter router);
4: public Task Invoke(HttpContext httpContext);
5: }
除了檢驗請求是否與自身設置的路由規則相匹配,併在成功匹配的情況下解析出路由參數並指定請求處理器之外,Router的路由解析還為另一個領用場景服務,那就是根據自身的路由規則和提供的參數生成一個URL。我們把這兩個方面稱為路由的兩個“方向”,它們分別對應著RouteDirection枚舉的兩個選項。針對這兩個方向的路由解析分別實現在IRouter的如下兩個方法(RouteAsync和GetVirtualPath),目前我們主要關註針對前者的RouteAsync方法。
1: public interface IRouter
2: {
3: Task RouteAsync(RouteContext context);
4: VirtualPathData GetVirtualPath(VirtualPathContext context);
5: }
6:
7: public enum RouteDirection
8: {
9: IncomingRequest,
10: UrlGeneration
11: }
如上面的代碼片段所示,針對請求實施路由解析的RouteAsync方法的輸入參數是一個類型為RouteContext的上下文對象。這個RouteContext實際上是對一個HttpContext對象的封裝,Router可以利用它得到所有與當前請求相關的信息。如果Router完成路由解析並判斷當前請求與自身的路由規則一致,那麼它會將解析出來的路由參數轉換成一個RouteData並存放到RouteContext對象代表的上下文之中,另一個一併被放入上下文的是代表當前請求處理器的RequestDelegate對象。下圖基本上展示了RouteAsync方法試試路由解析的原理。
二、RouteContext
接下來我們來瞭解一下整個路由解析涉及到了幾個核心類型,首先來看看為整個路由解析提供執行上下文的這個RouteContext類型。如上圖所示,一個RouteContext上下文包含三個核心對象,一個是代表當前請求上下文的HttpContext對象,對應的屬性是HttpContext。它實際上是作為路由解析的輸入,併在RouteContext創建的時候以構造函數參數的形式提供。另外兩個則是作為路由解析的輸出,一個是代表存放路由參數的RouteData對象,另一個則是作為請求處理器的RequestDelegate對象,對應的屬性分別是RouteData和Handler。
1: public class RouteContext
2: {
3: public HttpContext HttpContext { get; }
4: public RouteData RouteData { get; set; }
5: public RequestDelegate Handler { get; set; }
6:
7: public RouteContext(HttpContext httpContext);
8: }
三、RouteData
我們先來看看用於存放路由參數的RouteData類型。從數據來源的角度來講,路由參數具有兩種類型,一種是通過請求路徑攜帶的參數,另一種則是Router對象自身攜帶的參數,這兩種路由參數分別對應著RouteData的Values和DataTonkens屬性。至於另一個屬性Routers,則保存著實施路由解析並提供路由參數的所有Router對象。
1: public class RouteData
2: {
3: public RouteValueDictionary Values { get; }
4: public RouteValueDictionary DataTokens { get; }
5: public IList<IRouter> Routers { get; }
6: }
7:
8: public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
9: {
10: public RouteValueDictionary(object values);
11: …
12: }
從上面的代碼片可以看出,RouteData的Values和DataTokens屬性的類型都是RouteValueDictionary,它實際上就是一個字典對象而已,其Key和Value分別代表路由參數的名稱和值,而作為Key的字元串是不區分大小寫的。值得一提的是RouteValueDictionary具有一個特殊的構造函數,作為唯一參數的是一個object類型的對象。如果我們指定的參數是一個RouteValueDictionary對象或者是一個元素類型為KeyValuePair<string, object>>的集合,指定的數據將會作為原始數據源。這個特性體現在如下所示的調試斷言中。
1: var values1 = new RouteValueDictionary() ;
2: values1.Add("foo", 1);
3: values1.Add("bar", 2);
4: values1.Add("baz", 3);
5:
6: var values2 = new RouteValueDictionary(values1);
7: Debug.Assert(int.Parse(values2["foo"].ToString()) == 1);
8: Debug.Assert(int.Parse(values2["bar"].ToString()) == 2);
9: Debug.Assert(int.Parse(values2["baz"].ToString()) == 3);
10:
11: values2 = new RouteValueDictionary(new Dictionary<string, object>
12: {
13: ["foo"] = 1,
14: ["bar"] = 2,
15: ["baz"] = 3,
16: });
17: Debug.Assert(int.Parse(values2["foo"].ToString()) == 1);
18: Debug.Assert(int.Parse(values2["bar"].ToString()) == 2);
19: Debug.Assert(int.Parse(values2["baz"].ToString()) == 3);
RouteValueDictionary的這個構造函數的特殊之處其實並不止於此。除了將一個自身具有字典結構的對象作為原始數據源作為參數之外,我們還可以將一個普通的對象作為參數,在此情況下這個構造函數會解析定義在對象自身類型的所有屬性定義,並將屬性名稱和值作為路由參數的名稱和值。如下麵的代碼片段所示,我們創建一個匿名類型的對象並根據它來創建一個RouteValueDictionary,這種方式在MVC應用使用得比較多。
1: RouteValueDictionary values = new RouteValueDictionary(new
2: {
3: Foo = 1,
4: Bar = 2,
5: Baz = 3
6: });
7:
8: Debug.Assert(int.Parse(values["foo"].ToString()) == 1);
9: Debug.Assert(int.Parse(values["bar"].ToString()) == 2);
10: Debug.Assert(int.Parse(values["baz"].ToString()) == 3);
由於RouteData被直接置於RouteContext這上下文中,所以任何可以訪問到這個上下文的對象都可以隨意地修改其中的路由參數,為了全局對象造成的“數據污染”問題,一種類型與“快照”的策略被應用到RouteData上。具體來說,我們為某個RouteData當前的狀態創建一個快照,在後續的某個時刻我們利用這個快照讓這個RouteData對象回覆到當初的狀態。
針對RouteData的這個快照通過具有如下定義的結構RouteDataSnapshot表示。當我們創建這個一個對象的時候,需要指定目標RouteData對象和當前的狀態(Values、DataTokens和Routers)。當我們調用其Restore方法的時候,目標RouteData將會恢復到快照創建時的狀態。我們可以直接調用RouteData的PushState為它自己創建一個快照。
1: public struct RouteDataSnapshot
2: {
3: public RouteDataSnapshot(RouteData routeData, RouteValueDictionary dataTokens, IList<IRouter> routers, RouteValueDictionary values);
4: public void Restore();
5: }
6:
7: public class RouteData
8: {
9: public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens);
10: }
如下麵的代碼片段所示,我們創建了一個RouteData對象並調用其PushState方法為它創建了一個快照,調用該方法指定的三個參數均為null。雖然我們在後續步驟中修改了這個RouteData的狀態,但是一旦我們調用了這個RouteDataSnapshot對象的Restore方法,這個RouteData將重新恢復到最初的狀態。
1: RouteData routeData = new RouteData();
2: RouteDataSnapshot snapshot = routeData.PushState(null, null, null);
3:
4: routeData.Values.Add("foo", 1);
5: routeData.DataTokens.Add("bar", 2);
6: routeData.Routers.Add(new RouteHandler(null));
7:
8: snapshot.Restore();
9: Debug.Assert(!routeData.Values.Any());
10: Debug.Assert(!routeData.DataTokens.Any());
11: Debug.Assert(!routeData.Routers.Any());
四、Route
除了IRouter這個最為基礎的介面之外,路由系統中還定義了額外一些介面和抽象類,其中就包含如下這個INamedRouter介面。這個介面代表一個“具名的”Router,說白了就是這個Router具有一個通過屬性Name表示的名字。
1: public interface INamedRouter : IRouter
2: {
3: string Name { get; }
4: }
所有具體的Route基本上都最終繼承自如下這個抽象基類RouteBase,前面演示實例體現的基於“路由模板”的路由解析策略就體現在這個類型中。如下麵的代碼片段所示,RouterBase實現了INamedRouter介面,所以它具有一個名稱作為標識。它的ParsedTemplate屬性返回的RouteTemplate對象表示這個路由模板,它的Defaults和Constraints則是針對以內聯方式設置的預設值和約束的解析結果。針對內聯約束的解析是利用一個InlineConstraintResolver對象來完成的,RouteBase的ConstraintResolver屬性返回就是這麼一個對象。RouteData的DataTokens來源於Router對象,對應的屬性就是DataTokens。
1: public abstract class RouteBase : INamedRouter
2: {
3: public virtual string Name { get; protected set; }
4: public virtual RouteTemplate ParsedTemplate { get; protected set; }
5: protected virtual IInlineConstraintResolver ConstraintResolver { get; set; }
6:
7: public virtual RouteValueDictionary DataTokens { get; protected set; }
8: public virtual RouteValueDictionary Defaults { get; protected set; }
9: public virtual IDictionary<string, IRouteConstraint> Constraints { get; protected set; }
10: