ASP.NET Core應用中的路由機制實現在RouterMiddleware中間件中,它的目的在於通過路由解析為請求找到一個匹配的處理器,同時將請求攜帶的數據以路由參數的形式解析出來供後續請求處理流程使用。但是具體的路由解析功能其實並沒有直接實現在RouterMiddleware中間件中,而是由一個Router對象來完成的。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、IRouter接口
二、RouteContext
三、RouteData
四、Route
五、RouteHandler
總結
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上下文包含三個核心對象,一個是代表當前請求上下文的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類型。從數據來源的角度來講,路由參數具有兩種類型,一種是通過請求路徑攜帶的參數,另一種則是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());
除了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:
11: public RouteBase(string template, string name, IInlineConstraintResolver constraintResolver, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens);
12:
13: public virtual Task RouteAsync(RouteContext context);
14: protected abstract Task OnRouteMatched(RouteContext context);
15: …
16: }
對於實現在 RouteAsync方法中針對入棧請求而進行的路由解析,RouteBase中的實現只負責判斷是否給定的條件是否滿足自身的路由規則,並在規則滿足的情況下將解析出來的路由參數保存到RouteContext這個上下文中。至於滿足路由規則情況下實施的後續操作, 則實現在抽象方法OnRouteMatched中。
我們在進行路由注冊的時候經常使用的Route類型是具有如下定義的Route它是上面這個抽象類RouteBase子類。從如下的代碼片段我們不難看出,一個Route對象其實是對另一個Router對象的封裝,它自身並沒有承載任何具體的路由功能。我們在創建這個Route對象的時候,需要提供這個被封裝的Router,這個Router對象在重寫的OnRouteMatched方法中被添加到RouteData的Routers屬性中,隨後它的RouteAsync方法被執行。
1: public class Route : RouteBase
2: {
3: private readonly IRouter _target;
4: public string RouteTemplate
5: {
6: get { return this.ParsedTemplate.TemplateText; }
7: }
8:
9: public Route(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : this(target, routeTemplate, null, null, null, inlineConstraintResolver){}
10:
11: public Route(IRouter target, string routeTemplate, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver)
12: : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver){}
13:
14: public Route(IRouter target, string routeName, string routeTemplate, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver)
15: : base(routeTemplate, routeName, inlineConstraintResolver, defaults, constraints, dataTokens)
16: {
17: _target = target;
18: }
19:
20: protected override Task OnRouteMatched(RouteContext context)
21: {
22: context.RouteData.Routers.Add(_target);
23: return _target.RouteAsync(context);
24: }
25:
26: protected override VirtualPathData OnVirtualPathGenerated(VirtualPathContext context)
27: {
28: return _target.GetVirtualPath(context);
29: }
30: }
一個Router在進行針對請求的路由解析過程中需要判斷當前請求是否與自身設置的路由規則相匹配,並在匹配情況下將解析出來的路由參數存放與RouteContext這個上下文中,這些都實現在RouteBase這個基類中。由於Route派生於RouteBase,所以它自身也提供了這項基本功能。但是Router還具有另一個重要的任務,那就是在路由匹配情況下將作為處理器的RequestDelegate對象存放到RouteContext上下文中,這個任務最終落實到RouteHandler這個特殊的Router上。
RouteHandler是一種特殊的Router類型,它不僅實現了IRouter接口,還同時實現了另一個IRouteHandler接口,後者提供了一個GetRequestHandler方法根據表示當前請求上下文的HttpContext對象和封裝了路由參數的RouteData對象得到一個RequestDelegate對象,後者將會用來處理當前請求。如下面的代碼片段所示,我們創建一個RouteHandler對象是需要顯式指定一個RequestDelegate對象,GetRequestHandler方法返回的正是這個對象。在實現的RouteAsync方法中,它將這個RequestDelegate賦值給RouteContext的Handler屬性。
1: public class RouteHandler : IRouteHandler, IRouter
2: {
3: private readonly RequestDelegate _requestDelegate;
4:
5: public RouteHandler(RequestDelegate requestDelegate)
6: {
7: _requestDelegate = requestDelegate;
8: }
9:
10: public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData)
11: {
12: return _requestDelegate;
13: }
14:
15: public Task RouteAsync(RouteContext context)
16: {
17: context.Handler = _requestDelegate;
18: return Task.CompletedTask;
19: }
20: }
21:
22: public interface IRouteHandler
23: {
24: RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData);
25: }
基類RouteBase能夠確定當前請求是否與自身設置的路由規則相匹配,並在匹配的情況下設置路由參數,而RouteHandler只提供設置請求處理器的功能,但是一個真正的Router必須同時具有這兩項功能,那麼後者究竟是怎樣一個對象呢?我們在上面介紹繼承自RouteBase的Route類型時,我們說一個Route對象是對另一個Router對象的封裝,那麼被封裝的Router如果是一個RouteHanlder,那麼這個Route對象不就具有完整的路由解析功能了嗎?
我們介紹了一系列與Router相關的接口和類,包括IRouter、INameRouter和IRouteHandler接口,抽象類RouteBase,以及兩個具體的Route和RouteHandler類性。這些與Router相關額接口和類性具有如下圖所示的關系。