當某個請求能夠被成功路由的前提是它滿足某個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;
12: string resourceName = values["resourceName"].ToString().ToLower();
13: await context.Response.WriteAsync(Resources.ResourceManager.GetString(resourceName));
14: });
15:
16: new WebHostBuilder()
17: .UseKestrel()
18: .ConfigureServices(svcs => svcs
19: .AddRouting()
20: .Configure<RouteOptions>(options=>options.ConstraintMap.Add("culture", typeof(CultureConstraint))))
21: .Configure(app =>app.UseRouter(builder=> builder.MapRoute(template, action)))
22: .Build()
23: .Run();
24: }
25: }
我們通過調用擴展方法MapRoute注冊了這個路由。利用作為參數的Action<IApplicationBuilder>對象,我們注冊了一個自定義的LocalizationMiddleware中間件,這個中間件實現針對多語言的本地化。至于資源內容的響應,我們將它實現在通過調用ApplicationBuilder的Run方法注冊的中間件上。我們從解析出來的路由參數中獲取目標資源條目的名稱,然后利用資源文件自動生成的Resoruces類型獲取對應的資源內容并響應給客戶端。
在揭秘CultureConstraint這個自定義路由約束以及LocalizationMiddleware中間件的實現原理之前,我們先來看看客戶端采用是采用怎樣的形式獲取某個資源條目針對某種語言的內容。如下圖所示,我們直接利用瀏覽器采用與注冊路由相匹配的URL(“/resources/en/hello”或者“/resources/zh/hello”)不僅可以獲取目標資源的內容,顯示的語言也與我們指定的語言文化一致。如果指定一個不合法的語言(比如“xx”),將會違反我們自定義的約束,此時就會得到一個狀態碼為“404 Not Found”的響應。
接下來我們來看看這個針對語言文化的路由約束CultureConstraint就是做了些什么。如下面的代碼片段所示,我們在Match方法中會試圖獲取作為語言文化內容的路由參數值,如果這樣的路由參數存在,我們會利用它創建一個CultureInfo對象。如果這個CultureInfo的EnglishName屬性名不以“Unknown Language”字符作為前綴,我們就認為指定的是合法的語言文件。
1: public class CultureConstraint : IRouteConstraint
2: {
3: public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
4: {
5: try
6: {
7: object value;
8: if (values.TryGetValue(routeKey, out value))
9: {
10: return !new CultureInfo(value.ToString()).EnglishName.StartsWith("Unknown Language");
11: }
12: return false;
13: }
14: catch
15: {
16: return false;
17: }
18: }
19: }
我們.NET應用在運行的時候具有根據當前線程的語言文化選擇資源文件的能力。就我們這實例提供的兩個資源文件(Resources.resx和Resources.zh.resx)來說,如果當前線程的UICulture屬性代表的是一個針對“zh”的語言文化,資源文件Resources.zh.resx會被選擇。對于其他語言文件,則被選擇的就是這個Resources.resx文件。換句話說,如果我們要讓運行時選擇某個我們希望的資源文件,我們可以為當前線程設置相應的語言文化,實際上LocalizationMiddleware這個中間件就是這么做的。
1: public class LocalizationMiddleware
2: {
3: private RequestDelegate _next;
4: private string _routeKey;
5:
6: public LocalizationMiddleware(RequestDelegate next, string routeKey)
7: {
8: _next = next;
9: _routeKey = routeKey;
10: }
11:
12: public async Task Invoke(HttpContext context)
13: {
14: object culture;
15: CultureInfo currentCulture = CultureInfo.CurrentCulture;
16: CultureInfo currentUICulture = CultureInfo.CurrentUICulture;
17: try
18: {
19: if (context.GetRouteData().Values.TryGetValue(_routeKey, out culture))
20: {
21: CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = new CultureInfo(culture.ToString());
22: }
23: await _next(context);
24: }
25: finally
26: {
27: CultureInfo.CurrentCulture = currentCulture;
28: CultureInfo.CurrentUICulture = currentUICulture;
29: }
30: }
31: }
如上面的代碼片段所示,LocalizationMiddleware的Invoke方法被執行的時候,它會試圖從路由參數中得到目標語言,代表路由參數名稱的字段_routeKey是在構造函數中初始化的。如果這樣的路由參數存在,它會據此創建一個CultureInfo對象并將其作為當前線程的Culture和CultureInfo屬性。值得一提的是,在完成后續請求處理流程之后,我們需要將當前線程的語言文化恢復到之前的狀態。
ASP.NET Core的路由[1]:注冊URL模式與HttpHandler的映射關系
ASP.NET Core的路由[2]:路由系統的核心對象——Router
ASP.NET Core的路由[3]:Router的創建者——RouteBuilder
ASP.NET Core的路由[4]:來認識一下實現路由的RouterMiddleware中間件
ASP.NET Core的路由[5]:內聯路由約束的檢驗
文章列表