ASP.NET Core的路由是通過一個類型為RouterMiddleware的中間件來實現的。如果我們將最終處理HTTP請求的組件稱為HttpHandler,那麼RouterMiddleware中間件的意義在於實現請求路徑與對應HttpHandler之間的映射關系。對於傳遞給RouterMiddleware中間件的每一個請求,它會通過分析請求URL的模式並選擇並提取對應的HttpHandler來處理該請求。除此之外,請求的URL還會攜帶相應參數,該中間件在進行路由解析過程中還會根據生成相應的路由參數提供給處理該請求的Handler。為了讓讀者朋友們對實現在RouterMiddleware的路由功能具有一個大體的認識,我們照例先來演示幾個簡單的實例。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、注冊請求路徑與HttpHandler之間的映射
二、設置內聯約束
三、為路由參數設置默認值
四、特殊的路由參數
ASP.NET Core針對請求的處理總是在一個通過HttpContext對象表示的上下文中進行,所以上面我們所說的HttpHandler從編程的角度來講體現為一個RequestDelegate的委托對象,因此所謂的“路由注冊”就是注冊一組具有相同默認的請求路徑與對應RequestDelegate之間的映射關系。接下來我們就同一個簡單的實例來演示這樣的映射關系是如何通過注冊RouterMiddleware中間件的方式來完成的。
我們演示的這個ASP.NET Core應用是一個簡易版的天氣預報站點。如果用戶希望獲取某個城市在未來N天之內的天氣信息,他可以直接利用浏覽器發送一個GET請求並將對應城市(采用電話區號表示)和天數設置在URL中。如下圖所示,為了得到成都未來兩天的天氣信息,我們發送請求采用的路徑為“weather/028/2”。對於路徑“weather/0512/4”的請求,返回的自然就是蘇州未來4天的添加信息。
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: }
我們說最終用於處理請求的HttpHandler最終體現為一個類型為RequestDelegate的委托對象,為此我們定義了如下一個與這個委托類型具有一致聲明的方法WeatherForecast來處理針對天氣的請求。如下面的代碼片段所示,我們在這個方法中直接調用HttpContext的擴展方法GetRouteData得到RouterMiddleware中間件在路由解析過程中得到的路由參數。這個GetRouteData方法返回的是一個具有字典結構的對象,它的Key和Value分別代表路由參數的名稱和值,我們通過預先定義的參數名(“city”和“days”)得到目標城市和預報天數。
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: }
有了這兩個核心參數之後,我們據此生成一個WeatherReport對象,並將它攜帶的天氣信息以一個HTML文檔的形式響應給客戶端,圖1所示就是這個HTML文檔在浏覽器上的呈現效果。由於目標城市最初以電話區號的形式體現,在呈現天氣信息的過程中我們還會根據區號獲取具體城市名稱,簡單起見,我們利用一個簡單的字典來保存區號和城市之間的關系,並且只存儲了三個城市而已。
接下來我們來完成所需的路由注冊工作,實際上就是注冊RouterMiddleware中間件。由於這各中間件定義在“Microsoft.AspNetCore.Routing”這個NuGet包中,所以我們需要添加對應的依賴。如下面的代碼片段所示,針對RouterMiddleware中間件的注冊實現在ApplicationBuilder的擴展方法UseRouter中。由於RouterMiddleware中間件在進行路由解析的過程中需要使用到一些服務,我們調用WebHostBuilder的ConfigureServices方法注冊的就是這些服務。具體來說,這些與路由相關的服務是通過調用ServiceCollection的擴展方法AddRouting實現的。
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中間件針對路由的解析依賴於一個名為Router的對象,對應的接口為IRouter。我們在程序中會先根據ApplicationBuilder對象創建一個RouteBuilder對象,並利用後者來創建這個Router。我們說路由注冊從本質上體現為注冊某種URL模式與一個RequestDelegate對象之間的映射,這個映射關系的建立是通過調用RouteBuilder的MapGet方法的調用。MapGet方法具有兩個參數,第一個參數代表映射的URL模板,後者是處理請求的RequestDelegate對象。我們指定的URL模板為“weather/{city}/{days}”,其中攜帶兩個路由參數({city}和{days}),我們知道它代表獲取天氣預報的目標城市和天數。由於針對天氣請求的處理實現在我們定義的WeatherReport方法中,我們將指向這個方法的RequestDelegate對象作為第二個參數。
在上面進行路由注冊的實例中,我們在注冊的URL模板中定義了兩個參數({city}和{days})來分別代表獲取天氣預報的目標城市對應的區號和天數。區號應該具有一定的格式(以零開始的3-4位數字),而天數除了必須是一個整數之外,還應該具有一定的范圍。由於我們在注冊的時候並沒有為這個兩個路由參數的取值做任何的約束,所以請求URL攜帶的任何字符都是有效的。而處理請求的WeatherForecast方法也並沒有對提取的數據做任何的驗證,所以在執行過程中會直接拋出異常。如下圖所示,由於請求URL(“/weather/0512/iv”)指定了天數不合法,所有客戶端接收到一個狀態為“500 Internal Server Error”的響應。
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();
如果我們在注冊路由的時候應用了約束,那麼當RouterMiddleware中間件在進行路由解析的時候除了要求請求路徑必須與路由模板具有相同的模式,同時還要求攜帶的數據滿足對應路由參數的約束條件。如果不能同時滿足這兩個條件,RouterMiddleware中間件將無法選擇一個RequestDelegate對象來處理當前請求,在此情況下它將直接將請求遞交給後續的中間件進行處理。對於我們演示的這個實例來說,如果我們提供一個不合法的區號(1014)和預報天數(5),客戶端都將得到一個狀態碼為“404 Not Found”的響應。
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();
既然可以路由變量占據的部分路徑是可以缺省的,那麼意味即使請求的URL不具有對應的內容(比如“weather”和“weather/010”),在進行路由解析的時候同樣該請求與路由規則相匹配,但是在最終的路由參數字典中將找不到它們。由於表示目標城市和預測天數的兩個路由參數都是可缺省的,我們需要對處理請求的WeatherForecast方法做作相應的改動。下面的代碼片段表明如果請求URL為顯式提供對應參數的數據,它們的默認值分別為“010”(北京)和4(天),也就是說應用默認提供北京地區未來四天的天氣。
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: }
針對上述的改動,如果希望獲取北京未來四天的天氣狀況,我們可以采用如下圖所示的三種URL(“weather”和“weather/010”和“weather/010/4”),它們都是完全等效的。
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();
一個URL可以通過分隔符“/”劃分為多個路徑分段(Segment),路由模板中定義的路由參數一般來說會占據某個獨立的分段(比如“weather/{city}/{days}”)。不過也有特例,我們即可以在一個單獨的路徑分段中定義多個路由參數,同樣也可以讓一個路由參數跨越對個連續的路徑分段。
我們先來介紹在一個獨立的路徑分段中定義多個路由參數的情況。同樣以我們演示的獲取天氣預報的URL為例,假設我們設計一種URL來獲取某個城市某一天的天氣信息,比如“/weather/010/2016.11.11”這樣一個URL可以獲取北京地區在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采用了新的設計,所以我們按照如上的形式對相關的程序進行了相應的修改。現在我們采用匹配的URL(比如“/weather/010/2016.11.11”)就可以獲取到某個城市指定日期的天氣。
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: }
我們可以對程序做如上的修改來使用新的URL模板(“/weather/{city}/{*date}”)。這樣為了得到如上圖所示的北京在2016年11月11日的天氣,請求的URL可以替換成“/weather/010/2016/11/11”。