ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系
ASP.NET Core的路由是通過(guò)一個(gè)類(lèi)型為RouterMiddleware的中間件來(lái)實(shí)現(xiàn)的。如果我們將最終處理HTTP請(qǐng)求的組件稱(chēng)為HttpHandler,那么RouterMiddleware中間件的意義在于實(shí)現(xiàn)請(qǐng)求路徑與對(duì)應(yīng)HttpHandler之間的映射關(guān)系。對(duì)于傳遞給RouterMiddleware中間件的每一個(gè)請(qǐng)求,它會(huì)通過(guò)分析請(qǐng)求URL的模式并選擇并提取對(duì)應(yīng)的HttpHandler來(lái)處理該請(qǐng)求。除此之外,請(qǐng)求的URL還會(huì)攜帶相應(yīng)參數(shù),該中間件在進(jìn)行路由解析過(guò)程中還會(huì)根據(jù)生成相應(yīng)的路由參數(shù)提供給處理該請(qǐng)求的Handler。為了讓讀者朋友們對(duì)實(shí)現(xiàn)在RouterMiddleware的路由功能具有一個(gè)大體的認(rèn)識(shí),我們照例先來(lái)演示幾個(gè)簡(jiǎn)單的實(shí)例。[本文已經(jīng)同步到《ASP.NET Core框架揭秘》之中]
目錄
一、注冊(cè)請(qǐng)求路徑與HttpHandler之間的映射
二、設(shè)置內(nèi)聯(lián)約束
三、為路由參數(shù)設(shè)置默認(rèn)值
四、特殊的路由參數(shù)
一、注冊(cè)請(qǐng)求路徑與HttpHandler之間的映射
ASP.NET Core針對(duì)請(qǐng)求的處理總是在一個(gè)通過(guò)HttpContext對(duì)象表示的上下文中進(jìn)行,所以上面我們所說(shuō)的HttpHandler從編程的角度來(lái)講體現(xiàn)為一個(gè)RequestDelegate的委托對(duì)象,因此所謂的“路由注冊(cè)”就是注冊(cè)一組具有相同默認(rèn)的請(qǐng)求路徑與對(duì)應(yīng)RequestDelegate之間的映射關(guān)系。接下來(lái)我們就同一個(gè)簡(jiǎn)單的實(shí)例來(lái)演示這樣的映射關(guān)系是如何通過(guò)注冊(cè)RouterMiddleware中間件的方式來(lái)完成的。
我們演示的這個(gè)ASP.NET Core應(yīng)用是一個(gè)簡(jiǎn)易版的天氣預(yù)報(bào)站點(diǎn)。如果用戶(hù)希望獲取某個(gè)城市在未來(lái)N天之內(nèi)的天氣信息,他可以直接利用瀏覽器發(fā)送一個(gè)GET請(qǐng)求并將對(duì)應(yīng)城市(采用電話(huà)區(qū)號(hào)表示)和天數(shù)設(shè)置在URL中。如下圖所示,為了得到成都未來(lái)兩天的天氣信息,我們發(fā)送請(qǐng)求采用的路徑為“weather/028/2”。對(duì)于路徑“weather/0512/4”的請(qǐng)求,返回的自然就是蘇州未來(lái)4天的添加信息。
為了實(shí)現(xiàn)這個(gè)簡(jiǎn)單的應(yīng)用,我們定義如下一個(gè)名為WeatherReport的類(lèi)型表示某個(gè)城市在某段時(shí)間范圍類(lèi)的天氣。如下面的代碼片段所示,我們定義了另一個(gè)名為WeatherInfo的類(lèi)型來(lái)表示具體某一天的天氣。簡(jiǎn)單起見(jiàn),我們讓這個(gè)WeatherInfo對(duì)象只攜帶基本添加狀況和氣溫區(qū)間的信息。當(dāng)我們創(chuàng)建一個(gè)WeatherReport對(duì)象的時(shí)候,我們會(huì)隨機(jī)生成這些天氣信息。
1: public class WeatherReport 2: { 3: private static string[] _conditions = new string[] { "晴", "多云", "小雨" }; 4: private static Random _random = new Random(); 5:? 6: public string City { get; } 7: public IDictionary<DateTime, WeatherInfo> WeatherInfos { get; } 8:? 9: public WeatherReport(string city, int days) 10: { 11: this.City = city; 12: this.WeatherInfos = new Dictionary<DateTime, WeatherInfo>(); 13: for (int i = 0; i < days; i++) 14: { 15: this.WeatherInfos[DateTime.Today.AddDays(i + 1)] = new WeatherInfo 16: { 17: Condition = _conditions[_random.Next(0, 2)], 18: HighTemperature = _random.Next(20, 30), 19: LowTemperature = _random.Next(10, 20) 20: }; 21: } 22: } 23:? 24: public WeatherReport(string city, DateTime date) 25: { 26: this.City = city; 27: this.WeatherInfos = new Dictionary<DateTime, WeatherInfo> 28: { 29: [date] = new WeatherInfo 30: { 31: Condition = _conditions[_random.Next(0, 2)], 32: HighTemperature = _random.Next(20, 30), 33: LowTemperature = _random.Next(10, 20) 34: } 35: }; 36: } 37:? 38: public class WeatherInfo 39: { 40: public string Condition { get; set; } 41: public double HighTemperature { get; set; } 42: public double LowTemperature { get; set; } 43: } 44: }我們說(shuō)最終用于處理請(qǐng)求的HttpHandler最終體現(xiàn)為一個(gè)類(lèi)型為RequestDelegate的委托對(duì)象,為此我們定義了如下一個(gè)與這個(gè)委托類(lèi)型具有一致聲明的方法WeatherForecast來(lái)處理針對(duì)天氣的請(qǐng)求。如下面的代碼片段所示,我們?cè)谶@個(gè)方法中直接調(diào)用HttpContext的擴(kuò)展方法GetRouteData得到RouterMiddleware中間件在路由解析過(guò)程中得到的路由參數(shù)。這個(gè)GetRouteData方法返回的是一個(gè)具有字典結(jié)構(gòu)的對(duì)象,它的Key和Value分別代表路由參數(shù)的名稱(chēng)和值,我們通過(guò)預(yù)先定義的參數(shù)名(“city”和“days”)得到目標(biāo)城市和預(yù)報(bào)天數(shù)。
1: public class Program 2: { 3: private static Dictionary<string, string> _cities = new Dictionary<string, string> 4: { 5: ["010"] = "北京", 6: ["028"] = "成都", 7: ["0512"] = "蘇州" 8: }; 9:? 10: public static async Task WeatherForecast(HttpContext context) 11: { 12: string city = (string)context.GetRouteData().Values["city"]; 13: city = _cities[city]; 14: int days = int.Parse(context.GetRouteData().Values["days"].ToString()); 15: WeatherReport report = new WeatherReport(city, days); 16:? 17: context.Response.ContentType = "text/html"; 18: await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>"); 19: await context.Response.WriteAsync($"<h3>{city}</h3>"); 20: foreach (var it in report.WeatherInfos) 21: { 22: await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}:"); 23: await context.Response.WriteAsync($"{it.Value.Condition}({it.Value.LowTemperature}℃ ~ {it.Value.HighTemperature}℃)<br/><br/>"); 24: } 25: await context.Response.WriteAsync("</body></html>"); 26: } 27: … 28: }有了這兩個(gè)核心參數(shù)之后,我們據(jù)此生成一個(gè)WeatherReport對(duì)象,并將它攜帶的天氣信息以一個(gè)HTML文檔的形式響應(yīng)給客戶(hù)端,圖1所示就是這個(gè)HTML文檔在瀏覽器上的呈現(xiàn)效果。由于目標(biāo)城市最初以電話(huà)區(qū)號(hào)的形式體現(xiàn),在呈現(xiàn)天氣信息的過(guò)程中我們還會(huì)根據(jù)區(qū)號(hào)獲取具體城市名稱(chēng),簡(jiǎn)單起見(jiàn),我們利用一個(gè)簡(jiǎn)單的字典來(lái)保存區(qū)號(hào)和城市之間的關(guān)系,并且只存儲(chǔ)了三個(gè)城市而已。
接下來(lái)我們來(lái)完成所需的路由注冊(cè)工作,實(shí)際上就是注冊(cè)RouterMiddleware中間件。由于這各中間件定義在“Microsoft.AspNetCore.Routing”這個(gè)NuGet包中,所以我們需要添加對(duì)應(yīng)的依賴(lài)。如下面的代碼片段所示,針對(duì)RouterMiddleware中間件的注冊(cè)實(shí)現(xiàn)在ApplicationBuilder的擴(kuò)展方法UseRouter中。由于RouterMiddleware中間件在進(jìn)行路由解析的過(guò)程中需要使用到一些服務(wù),我們調(diào)用WebHostBuilder的ConfigureServices方法注冊(cè)的就是這些服務(wù)。具體來(lái)說(shuō),這些與路由相關(guān)的服務(wù)是通過(guò)調(diào)用ServiceCollection的擴(kuò)展方法AddRouting實(shí)現(xiàn)的。
1: public class Program 2: { 3: public static void Main() 4: { 5: new WebHostBuilder() 6: .UseKestrel() 7: .ConfigureServices(svcs => svcs.AddRouting()) 8: .Configure(app => app.UseRouter(builder => builder.MapGet("weather/{city}/{days}", WeatherForecast))) 9: .Build() 10: .Run(); 11: } 12: … 13: }RouterMiddleware中間件針對(duì)路由的解析依賴(lài)于一個(gè)名為Router的對(duì)象,對(duì)應(yīng)的接口為IRouter。我們?cè)诔绦蛑袝?huì)先根據(jù)ApplicationBuilder對(duì)象創(chuàng)建一個(gè)RouteBuilder對(duì)象,并利用后者來(lái)創(chuàng)建這個(gè)Router。我們說(shuō)路由注冊(cè)從本質(zhì)上體現(xiàn)為注冊(cè)某種URL模式與一個(gè)RequestDelegate對(duì)象之間的映射,這個(gè)映射關(guān)系的建立是通過(guò)調(diào)用RouteBuilder的MapGet方法的調(diào)用。MapGet方法具有兩個(gè)參數(shù),第一個(gè)參數(shù)代表映射的URL模板,后者是處理請(qǐng)求的RequestDelegate對(duì)象。我們指定的URL模板為“weather/{city}/{days}”,其中攜帶兩個(gè)路由參數(shù)({city}和{days}),我們知道它代表獲取天氣預(yù)報(bào)的目標(biāo)城市和天數(shù)。由于針對(duì)天氣請(qǐng)求的處理實(shí)現(xiàn)在我們定義的WeatherReport方法中,我們將指向這個(gè)方法的RequestDelegate對(duì)象作為第二個(gè)參數(shù)。
二、設(shè)置內(nèi)聯(lián)約束
在上面進(jìn)行路由注冊(cè)的實(shí)例中,我們?cè)谧?cè)的URL模板中定義了兩個(gè)參數(shù)({city}和{days})來(lái)分別代表獲取天氣預(yù)報(bào)的目標(biāo)城市對(duì)應(yīng)的區(qū)號(hào)和天數(shù)。區(qū)號(hào)應(yīng)該具有一定的格式(以零開(kāi)始的3-4位數(shù)字),而天數(shù)除了必須是一個(gè)整數(shù)之外,還應(yīng)該具有一定的范圍。由于我們?cè)谧?cè)的時(shí)候并沒(méi)有為這個(gè)兩個(gè)路由參數(shù)的取值做任何的約束,所以請(qǐng)求URL攜帶的任何字符都是有效的。而處理請(qǐng)求的WeatherForecast方法也并沒(méi)有對(duì)提取的數(shù)據(jù)做任何的驗(yàn)證,所以在執(zhí)行過(guò)程中會(huì)直接拋出異常。如下圖所示,由于請(qǐng)求URL(“/weather/0512/iv”)指定了天數(shù)不合法,所有客戶(hù)端接收到一個(gè)狀態(tài)為“500 Internal Server Error”的響應(yīng)。
為了確保路由參數(shù)數(shù)值的有效性,我們?cè)谶M(jìn)行路由注冊(cè)的時(shí)候可以采用內(nèi)聯(lián)(Inline)的方式直接將相應(yīng)的約束規(guī)則定義在路由模板中。ASP.NET Core針對(duì)我們常用的驗(yàn)證規(guī)則定義了相應(yīng)的約束表達(dá)式,我們可以根據(jù)需要為某個(gè)路由參數(shù)指定一個(gè)或者多個(gè)約束表達(dá)式。
如下面的代碼片段所示,為了確保URL攜帶的是合法的區(qū)號(hào),我們?yōu)槁酚蓞?shù){city}應(yīng)用了一個(gè)針對(duì)正則表達(dá)式的約束(:regex(^0[1-9]{{2,3}}$))。由于路由模板在被解析的時(shí)候會(huì)將“{…}”這樣的字符理解為路由參數(shù),如果約束表達(dá)式需要使用“{}”字符(比如正則表達(dá)式“^0[1-9]{2,3}$)”),需要采用“{{}}”進(jìn)行轉(zhuǎn)義。至于另一個(gè)路由參數(shù){days}則應(yīng)用了兩個(gè)約束,第一個(gè)是針對(duì)數(shù)據(jù)類(lèi)型的約束(:int),它要求參數(shù)值必須是一個(gè)整數(shù)。另一個(gè)是針對(duì)區(qū)間的約束(:range(1,4)),意味著我們的應(yīng)用最多只提供未來(lái)4天的天氣。
1: string template = @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}"; 2: new WebHostBuilder() 3: .UseKestrel() 4: .ConfigureServices(svcs => svcs.AddRouting()) 5: .Configure(app => app.UseRouter(builder=> builder.MapGet(template, WeatherForecast))) 6: .Build() 7: .Run();如果我們?cè)谧?cè)路由的時(shí)候應(yīng)用了約束,那么當(dāng)RouterMiddleware中間件在進(jìn)行路由解析的時(shí)候除了要求請(qǐng)求路徑必須與路由模板具有相同的模式,同時(shí)還要求攜帶的數(shù)據(jù)滿(mǎn)足對(duì)應(yīng)路由參數(shù)的約束條件。如果不能同時(shí)滿(mǎn)足這兩個(gè)條件,RouterMiddleware中間件將無(wú)法選擇一個(gè)RequestDelegate對(duì)象來(lái)處理當(dāng)前請(qǐng)求,在此情況下它將直接將請(qǐng)求遞交給后續(xù)的中間件進(jìn)行處理。對(duì)于我們演示的這個(gè)實(shí)例來(lái)說(shuō),如果我們提供一個(gè)不合法的區(qū)號(hào)(1014)和預(yù)報(bào)天數(shù)(5),客戶(hù)端都將得到一個(gè)狀態(tài)碼為“404 Not Found”的響應(yīng)。
三、為路由參數(shù)設(shè)置默認(rèn)值
路由注冊(cè)時(shí)提供的路由模板(比如“Weather/{city}/{days}”)可以包含靜態(tài)的字符(比如“weather”),也可以包括動(dòng)態(tài)的參數(shù)(比如{city}和{days}),我們將它們成為路由參數(shù)。并非每個(gè)路由參數(shù)都是必需的(要求路由參數(shù)的值必需存在請(qǐng)求路徑中),有的路由參數(shù)是可以缺省的。還是以上面演示的實(shí)例來(lái)說(shuō),我們可以采用如下的方式在路由參數(shù)名后面添加一個(gè)問(wèn)號(hào)(“?”),原本必需的路由參數(shù)變成了可以缺省的。可缺省的路由參數(shù)只能出現(xiàn)在路由模板尾部,這個(gè)應(yīng)該不難理解。
1: string template = "weather/{city?}/{days?}"; 2: new WebHostBuilder() 3: .UseKestrel() 4: .ConfigureServices(svcs => svcs.AddRouting()) 5: .Configure(app => app.UseRouter(builder=> builder.MapGet(template, WeatherForecast))) 6: .Build() 7: .Run();既然可以路由變量占據(jù)的部分路徑是可以缺省的,那么意味即使請(qǐng)求的URL不具有對(duì)應(yīng)的內(nèi)容(比如“weather”和“weather/010”),在進(jìn)行路由解析的時(shí)候同樣該請(qǐng)求與路由規(guī)則相匹配,但是在最終的路由參數(shù)字典中將找不到它們。由于表示目標(biāo)城市和預(yù)測(cè)天數(shù)的兩個(gè)路由參數(shù)都是可缺省的,我們需要對(duì)處理請(qǐng)求的WeatherForecast方法做作相應(yīng)的改動(dòng)。下面的代碼片段表明如果請(qǐng)求URL為顯式提供對(duì)應(yīng)參數(shù)的數(shù)據(jù),它們的默認(rèn)值分別為“010”(北京)和4(天),也就是說(shuō)應(yīng)用默認(rèn)提供北京地區(qū)未來(lái)四天的天氣。
1: public static async Task WeatherForecast(HttpContext context) 2: { 3: object rawCity; 4: object rawDays; 5: var values = context.GetRouteData().Values; 6: string city = values.TryGetValue("city", out rawCity) ? rawCity.ToString() : "010"; 7: int days = values.TryGetValue("days", out rawDays) ? int.Parse(rawDays.ToString()) : 4; 8: 9: city = _cities[city]; 10: WeatherReport report = new WeatherReport(city, days); 11: … 12: }針對(duì)上述的改動(dòng),如果希望獲取北京未來(lái)四天的天氣狀況,我們可以采用如下圖所示的三種URL(“weather”和“weather/010”和“weather/010/4”),它們都是完全等效的。
上面我們的程序相當(dāng)于是在進(jìn)行請(qǐng)求處理的時(shí)候給予了可缺省路由參數(shù)一個(gè)默認(rèn)值,實(shí)際上路由參數(shù)默認(rèn)值得設(shè)置還具有一種更簡(jiǎn)單的方式,那就是按照如下所示的方式直接將默認(rèn)值定義在路由模板中。如果采用這樣的路由注冊(cè)方式,我們針對(duì)WeatherForecast方法的改動(dòng)就完全沒(méi)有必要了。
1: string template = "weather/{city=010}/{days=4}"; 2: new WebHostBuilder() 3: .UseKestrel() 4: .ConfigureServices(svcs => svcs.AddRouting()) 5: .Configure(app =>app.UseRouter(builder=>builder.MapGet(template, WeatherForecast))) 6: .Build() 7: .Run();四、特殊的路由參數(shù)
一個(gè)URL可以通過(guò)分隔符“/”劃分為多個(gè)路徑分段(Segment),路由模板中定義的路由參數(shù)一般來(lái)說(shuō)會(huì)占據(jù)某個(gè)獨(dú)立的分段(比如“weather/{city}/{days}”)。不過(guò)也有特例,我們即可以在一個(gè)單獨(dú)的路徑分段中定義多個(gè)路由參數(shù),同樣也可以讓一個(gè)路由參數(shù)跨越對(duì)個(gè)連續(xù)的路徑分段。
我們先來(lái)介紹在一個(gè)獨(dú)立的路徑分段中定義多個(gè)路由參數(shù)的情況。同樣以我們演示的獲取天氣預(yù)報(bào)的URL為例,假設(shè)我們?cè)O(shè)計(jì)一種URL來(lái)獲取某個(gè)城市某一天的天氣信息,比如“/weather/010/2016.11.11”這樣一個(gè)URL可以獲取北京地區(qū)在2016年雙11那天的天氣,那么路由模板為“/weather/{city}/{year}.{month}.{day}”。
1: string tempalte = "weather/{city}/{year}.{month}.{day}"; 2: new WebHostBuilder() 3: .UseKestrel() 4: .ConfigureServices(svcs => svcs.AddRouting()) 5: .Configure(app => app.UseRouter(builder=>builder.MapGet(tempalte, WeatherForecast))) 6: .Build() 7: .Run(); 8:? 9: public static async Task WeatherForecast(HttpContext context) 10: { 11: var values = context.GetRouteData().Values; 12: string city = values["city"].ToString(); 13: city = _cities[city]; 14: int year = int.Parse(values["year"].ToString()); 15: int month = int.Parse(values["month"].ToString()); 16: int day = int.Parse(values["day"].ToString()); 17:? 18: WeatherReport report = new WeatherReport(city, new DateTime(year,month,day)); 19: … 20: }由于URL采用了新的設(shè)計(jì),所以我們按照如上的形式對(duì)相關(guān)的程序進(jìn)行了相應(yīng)的修改。現(xiàn)在我們采用匹配的URL(比如“/weather/010/2016.11.11”)就可以獲取到某個(gè)城市指定日期的天氣。
對(duì)于上面設(shè)計(jì)的這個(gè)URL來(lái)說(shuō),我們采用“.”作為日期分隔符,如果我們采用“/”作為日期分隔符(比如“2016/11/11”),這個(gè)路由默認(rèn)應(yīng)該如何定義呢?由于“/”同時(shí)也是URL得路徑分隔符,如果表示日期的路由變量也采用相同的分隔符,意味著同一個(gè)路由參數(shù)跨越了多個(gè)路徑分段,我們只能定義“通配符”路由參數(shù)的形式來(lái)達(dá)到這個(gè)目的。通配符路由參數(shù)采用“{*variable}”這樣的形式,星號(hào)(“*”)表示路徑“余下的部分”,所以這樣的路由參數(shù)只能出現(xiàn)在模板的尾端。對(duì)我們的實(shí)例來(lái)說(shuō),路由模板可以定義成“/weather/{city}/{*date}”。
1: new WebHostBuilder() 2: .UseKestrel() 3: .ConfigureServices(svcs => svcs.AddRouting()) 4: .Configure(app => { 5: string tempalte = "weather/{city}/{*date}"; 6: IRouter router = new RouteBuilder(app).MapGet(tempalte, WeatherForecast).Build(); 7: app.UseRouter(router); 8: }) 9: .Build() 10: .Run(); 11:? 12: public static async Task WeatherForecast(HttpContext context) 13: { 14: var values = context.GetRouteData().Values; 15: string city = values["city"].ToString(); 16: city = _cities[city]; 17: DateTime date = DateTime.ParseExact(values["date"].ToString(), "yyyy/MM/dd", 18: CultureInfo.InvariantCulture); 19: WeatherReport report = new WeatherReport(city, date); 20: … 21: }我們可以對(duì)程序做如上的修改來(lái)使用新的URL模板(“/weather/{city}/{*date}”)。這樣為了得到如上圖所示的北京在2016年11月11日的天氣,請(qǐng)求的URL可以替換成“/weather/010/2016/11/11”。
ASP.NET Core的路由[1]:注冊(cè)URL模式與HttpHandler的映射關(guān)系
ASP.NET Core的路由[2]:路由系統(tǒng)的核心對(duì)象——Router
ASP.NET Core的路由[3]:Router的創(chuàng)建者——RouteBuilder
ASP.NET Core的路由[4]:來(lái)認(rèn)識(shí)一下實(shí)現(xiàn)路由的RouterMiddleware中間件
ASP.NET Core的路由[5]:內(nèi)聯(lián)路由約束的檢驗(yàn) 與50位技術(shù)專(zhuān)家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C++ ## ... 实用
- 下一篇: 支持断线重连、永久watcher、递归操