當某個請求能夠被成功路由的前提是它滿足某個Route對象設置的路由規則,具體來說,當前請求的URL不僅需要滿足路由模板體現的路徑模式,請求還需要滿足Route對象的所有約束。路由系統採用IRouteConstraint介面來表示路由約束,所以我們在接下來的內容中將路由約束統稱為RouteConstr... ...
當某個請求能夠被成功路由的前提是它滿足某個Route對象設置的路由規則,具體來說,當前請求的URL不僅需要滿足路由模板體現的路徑模式,請求還需要滿足Route對象的所有約束。路由系統採用IRouteConstraint介面來表示路由約束,所以我們在接下來的內容中將路由約束統稱為RouteConstraint。 在大部分情況下,約束都是針對路由模板中定義的某個路由參數,其目的在於驗證URL攜帶的某部分的內容是否有效。不過也有一些約束與路由參數無關,這些約束規範往往是除URL之前的其他請求元素,比如前面提到的HttpMethodRouteConstraint檢驗的就是請求採用的方法。 [本文已經同步到《ASP.NET Core框架揭秘》之中]
1: public interface IRouteConstraint
2: {
3: bool Match(HttpContext httpContext, IRouter route, string routeKey,
4: RouteValueDictionary values, RouteDirection routeDirection);
5: }
如上面的代碼片段所示,IRouteConstraint介面僅僅定義瞭如下一個唯一的Match方法來定義約束規範。方法的參數分別是代表當前請求上下文的HttpContext、當前Router對象、約束在約束字典中的Key(對於針對路由參數的約束,這個Key就是路由參數的名稱)、從請求URL解析出來的所有路由參數和路由方向(針對入棧請求進行的路由解析還是為了生成URL而進行的路由解析)。
一、預定義RouteConstraint
路由系統定義了一系列原生的RouteConstraint類型,我們可以使用它們解決很多常見的約束問題,即使現有的RouteConstraint類型無法滿足某些特殊的約束需求,我們還可以自定義對應的RouteConstraint類型。對於路由約束的應用,除了直接創建對應的RouteConstraint對象之外,我們知道還可以採用內聯的方式直接在路由模板中定義為某個路由參數定義相應的約束表達式。這些以表達式定義的約束類型其實對應著一種具體的RouteConstraint類型。下表列出了兩者之間的匹配關係。
內聯約束類型 |
RouteConstraint類型 |
說明 |
int |
IntRouteConstraint |
要求路由參數值可能解析為一個int整數,比如{variable:int} |
bool |
BoolRouteConstraint |
要求參數值可以解析為一個bool值,比如{ variable:bool} |
datetime |
DateTimeRouteConstraint |
要求參數值可以解析為一個DateTime對象(採用CultureInfo. InvariantCulture進行解析),比如{ variable:datetime} |
decimal |
DecimalRouteConstraint |
要求參數值可以解析為一個decimal數字,比如{ variable:decimal} |
double |
DoubleRouteConstraint |
要求參數值可以解析為一個double數字,比如{ variable:double} |
float |
FloatRouteConstraint |
要求參數值可以解析為一個float數字,比如{ variable:float} |
guid |
GuidRouteConstraint |
要求參數值可以解析為一個Guid,比如{ variable:guid} |
long |
LongRouteConstraint |
要求參數值可以解析為一個long整數,比如{ variable:long} |
minlength |
MinLengthRouteConstraint |
要求參數值表示的字元串不於指定的長度{ variable:minlength(5)} |
maxlength |
MaxLengthRouteConstraint |
要求參數值表示的字元串不大於指定的長度,比如{ variable:maxlength(10)} |
length |
LengthRouteConstraint |
要求參數值表示的字元串長度限於指定的區間範圍,比如{ variable:length(5,10)} |
min |
MinRouteConstraint |
要求參數值不於指定的值,比如{ variable:min(5)} |
max |
MaxRouteConstraint |
要求參數值大於指定的值,比如{ variable:max(10)} |
range |
RangeouteConstraint |
要求參數值介於指定的區間範圍,比如{variable:range(5,10)} |
alpha |
AlphaRouteContraint |
要求參數值得所有字元都是字母,比如{variable:alpha} |
regex |
RegexInlineRouteConstraint |
要求參數值表示字元串與指定的正則表達式相匹配,比如{variable:regex(^d{0[0-9]{{2,3}-d{2}-d{4}$)}}}$)} |
required |
RequiredRouteConstraint |
要求參數值不應該是一個空字元串,比如{variable:required} |
RangeRouteConstraint
為了讓讀者朋友們對這些RouteConstraint具有更加深刻的理解,我們選擇一個用於限制變數值範圍的RangeRouteConstraint類進行單獨介紹。如下麵的代碼片斷所示,RangeRouteConstraint類型具有兩個長整型的只讀屬性Max和Min,它們分別表示約束範圍的上下限。
1: public class RangeRouteConstraint : IRouteConstraint
2: {
3: public long Max { get; }
4: public long Min { get; }
5: public RangeRouteConstraint(long min, long max)
6: {
7: this.Min = min;
8: this.Max = max;
9: }
10:
11: public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
12: {
13: object value;
14: if (values.TryGetValue(routeKey, out value) && value != null)
15: {
16: long longValue;
17: var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
18: if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
19: {
20: return longValue >= Min && longValue <= Max;
21: }
22: }
23: return false;
24: }
25: }
具體的約束檢驗實現在Match方法中。具體來說,RangeRouteConstraint根據被檢驗變數的名稱(對應於routeKey參數)從參數values(表示路由檢驗生成的所有路由變數)中提取被驗證的參數值,然後判斷它是否在通過屬性Max和Min表示的數值範圍內。
HttpMethodRouteConstraint
上面介紹的這些預定義的RouteConstraint類型都是對某個路由參數的值加以約束,除此之外還具有一個特殊的名為HttpMethodRouteConstraint的約束。我們在上面已經提到過,這個約束並不是應用在具有某個路由參數上,而是應用到整個請求上,它要求匹配的請求必須具有指定的方法。當我們在使用這種約束的時候,一般將對應的Key設置為“httpMethod”。
1: public class HttpMethodRouteConstraint : IRouteConstraint
2: {
3: public IList<string> AllowedMethods { get; }
4:
5: public HttpMethodRouteConstraint(params string[] allowedMethods)
6: {
7: this.AllowedMethods = new List<string>(allowedMethods);
8: }
9:
10: public virtual bool Match(HttpContext httpContext, IRouter route, string routeKey,RouteValueDictionary values, RouteDirection routeDirection)
11: {
12: switch (routeDirection)
13: {
14: case RouteDirection.IncomingRequest:return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
15:
16: case RouteDirection.UrlGeneration:
17: object obj;
18: if (!values.TryGetValue(routeKey, out obj))
19: {
20: return true;
21: }
22: return AllowedMethods.Contains(Convert.ToString(obj), StringComparer.OrdinalIgnoreCase);
23:
24: default:throw new ArgumentOutOfRangeException(nameof(routeDirection));
25: }
26: }
27: }
當我們在創建一個 HttpMethodRouteConstraint對象的時候,需要指定一個允許的HTTP方法列表。對於針對入棧請求的路由解析來說,HttpMethodRouteConstraint會檢驗當前請求採用的方法是否在這個列表之內。如果路由解析是為了生成URL,HttpMethodRouteConstraint會從指定的參數列表中提取指定的HTTP方法,如果這樣的參數存在,則會檢驗這個HTTP方法是否在允許的列表之內,否則意味著不需要針對HTTP方法進行驗證。
二、InlineConstraintResolver
如果在進行路由註冊的時候針對路由變數的約束是直接以內聯表達式的形式定義在路由模板中,所以路由系統需要解析約束表達式來創建對應類型的RouteConstraint對象,這項任務由一個叫做InlineConstraintResolver的對象來完成。所有的InlineConstraintResolver類型實現了具有如下定義的IInlineConstraintResolver介面,定義其中的唯一方法ResolveConstraint實現了約束從字元串表達式到RouteConstraint對象之間的轉換。
1: public interface IInlineConstraintResolver
2: {
3: IRouteConstraint ResolveConstraint(string inlineConstraint);
4: }
路由系統只定義了一個唯一的InlineConstraintResolver類型實現了這個介面,它就是DefaultInlineConstraintResolver類型。如下麵的代碼片斷所示,它具有一個字典類型的欄位_inlineConstraintMap,如表1所示的內聯約束類型與對應RouteConstraint類型之間的映射關係就保存在這個字典中。
1: public class DefaultInlineConstraintResolver : IInlineConstraintResolver
2: {
3: private readonly IDictionary<string, Type> _inlineConstraintMap;
4: public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
5: {
6: _inlineConstraintMap = routeOptions.Value.ConstraintMap;
7: }
8: public virtual IRouteConstraint ResolveConstraint(string inlineConstraint);
9: }
10:
11: public class RouteOptions
12: {
13: public IDictionary<string, Type> ConstraintMap { get; set; }
14: public bool LowercaseUrls { get; set; }
15: public bool AppendTrailingSlash { get; set; }
16: }
DefaultInlineConstraintResolver首先根據指定的約束表達式獲得以字元串表示的約束類型和參數列表。通過約束類型,它可以從ConstraintMap屬性表示的映射關係中得到對應的HttpRouteConstraint類型。接下來它根據參數個數得到匹配的構造函數,然後將字元串表示的參數轉換成對應的參數類型並以反射的形式將它們傳入構造函數創建相應的HttpRouteConstraint對象。
對於一個通過指定的路由模板創建的Route對象來說,當它在初始化的時候會利用ServiceProvider採用依賴註入的形式獲取這個InlineConstraintResolver對象來解析定義在路由模板中的內聯約束表達式,並將它們全部轉換成具體的RouteConstraint對象。這意味著在這之前,針對InlineConstraintResolver的服務註冊就以及存在,那麼這個服務是在什麼時候註冊的呢?
當我們在一個ASP.NET Core應用中使用路由功能的時候,除了需要註冊這個RouterMiddleware中間件之外,一般還需要調用ServiceCollection的擴展方法AddRouting註冊一些與路由相關的服務,針對InlineConstraintResolver的服務註冊就實現在這個方法之中。
三、自定義約束
我們可以使用上述這些預定義的RouteConstraint類們完成一些常用的約束檢驗,但是在一些對路由變數具有特殊的約束的應用場景中,我們不得不創建自定義的約束。舉個簡單的例子,如果我們需要對資源提供針對多語言的支持,最好的方式是在請求的URL中提供目標資源所針對的Culture。為了確保包含在URL中的是一個合法有效的Culture,我們最好為此定義相應的約束。
接下來,我們將通過一個簡單的實例來演示如何創建這麼一個用於驗證Culture的自定義約束。不過在這之前我們不妨先來看看使用這個約束最終實現的效果。在本例中我們創建了一個提供基於不同語言資源的Web API,簡單起見,我們僅僅提供針對相應Culture的文本數據。我們利用資源文件來作為文本資源的存儲,如下圖所示,我們在一個ASP.NET Core應用中創建了兩個資源文件Resources.resx(語言文化中性)和Resources.zh.resx(中文),並定義了一個名為“hello”的文本資源條目。
如下所示的是整個應用程式的定義。這段程式非常簡單,我們註冊了一個模板為“resources/{lang:culture}/{resourcename:required}”的路由。路由參數{ resourcename }表示獲取的資源條目的名稱(比如“hello”),這是一個必需的路由參數(應用了RequiredRouteConstraint約束)。另一個路由參數{lang}表示指定的語言,約束表達式名稱“culture”對應的就是我們自定義的針對語言文件的約束類型CultureConstraint。也正是因為是一個自定義的路由約束,我們必須將內聯約束表達式名稱和CultureConstraint類型之間的應用,我們在調用ConfigureServices方法中將這樣的映射添加到註冊的RouteOptions之中。
1: public class Program
2: {
3: public static void Main()
4: {
5: string template = "resources/{lang:culture}/{resourceName:required}";
6:
7: Action<IApplicationBuilder> action = app => app
8: .UseMiddleware<LocalizationMiddleware>("lang")
9: .Run(async context =>
10: {
11: var values = context.GetRouteData().Values;