文章出處

ASP.NET Core的路由是通過一個類型為RouterMiddleware的中間件來實現的。如果我們將最終處理HTTP請求的組件稱為HttpHandler,那么RouterMiddleware中間件的意義在于實現請求路徑與對應HttpHandler之間的映射關系。對于傳遞給RouterMiddleware中間件的每一個請求,它會通過分析請求URL的模式并選擇并提取對應的HttpHandler來處理該請求。除此之外,請求的URL還會攜帶相應參數,該中間件在進行路由解析過程中還會根據生成相應的路由參數提供給處理該請求的Handler。為了讓讀者朋友們對實現在RouterMiddleware的路由功能具有一個大體的認識,我們照例先來演示幾個簡單的實例。[本文已經同步到《ASP.NET Core框架揭秘》之中]

目錄
一、注冊請求路徑與HttpHandler之間的映射
二、設置內聯約束
三、為路由參數設置默認值
四、特殊的路由參數

一、注冊請求路徑與HttpHandler之間的映射

ASP.NET Core針對請求的處理總是在一個通過HttpContext對象表示的上下文中進行,所以上面我們所說的HttpHandler從編程的角度來講體現為一個RequestDelegate的委托對象,因此所謂的“路由注冊”就是注冊一組具有相同默認的請求路徑與對應RequestDelegate之間的映射關系。接下來我們就同一個簡單的實例來演示這樣的映射關系是如何通過注冊RouterMiddleware中間件的方式來完成的。

我們演示的這個ASP.NET Core應用是一個簡易版的天氣預報站點。如果用戶希望獲取某個城市在未來N天之內的天氣信息,他可以直接利用瀏覽器發送一個GET請求并將對應城市(采用電話區號表示)和天數設置在URL中。如下圖所示,為了得到成都未來兩天的天氣信息,我們發送請求采用的路徑為“weather/028/2”。對于路徑“weather/0512/4”的請求,返回的自然就是蘇州未來4天的添加信息。

1

為了實現這個簡單的應用,我們定義如下一個名為WeatherReport的類型表示某個城市在某段時間范圍類的天氣。如下面的代碼片段所示,我們定義了另一個名為WeatherInfo的類型來表示具體某一天的天氣。簡單起見,我們讓這個WeatherInfo對象只攜帶基本添加狀況和氣溫區間的信息。當我們創建一個WeatherReport對象的時候,我們會隨機生成這些天氣信息。

   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”的響應。

2

為了確保路由參數數值的有效性,我們在進行路由注冊的時候可以采用內聯(Inline)的方式直接將相應的約束規則定義在路由模板中。ASP.NET Core針對我們常用的驗證規則定義了相應的約束表達式,我們可以根據需要為某個路由參數指定一個或者多個約束表達式。

如下面的代碼片段所示,為了確保URL攜帶的是合法的區號,我們為路由參數{city}應用了一個針對正則表達式的約束(:regex(^0[1-9]{{2,3}}$))。由于路由模板在被解析的時候會將“{…}”這樣的字符理解為路由參數,如果約束表達式需要使用“{}”字符(比如正則表達式“^0[1-9]{2,3}$)”),需要采用“{{}}”進行轉義。至于另一個路由參數{days}則應用了兩個約束,第一個是針對數據類型的約束(:int),它要求參數值必須是一個整數。另一個是針對區間的約束(:range(1,4)),意味著我們的應用最多只提供未來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();

如果我們在注冊路由的時候應用了約束,那么當RouterMiddleware中間件在進行路由解析的時候除了要求請求路徑必須與路由模板具有相同的模式,同時還要求攜帶的數據滿足對應路由參數的約束條件。如果不能同時滿足這兩個條件,RouterMiddleware中間件將無法選擇一個RequestDelegate對象來處理當前請求,在此情況下它將直接將請求遞交給后續的中間件進行處理。對于我們演示的這個實例來說,如果我們提供一個不合法的區號(1014)和預報天數(5),客戶端都將得到一個狀態碼為“404 Not Found”的響應。

3

三、為路由參數設置默認值

路由注冊時提供的路由模板(比如“Weather/{city}/{days}”)可以包含靜態的字符(比如“weather”),也可以包括動態的參數(比如{city}和{days}),我們將它們成為路由參數。并非每個路由參數都是必需的(要求路由參數的值必需存在請求路徑中),有的路由參數是可以缺省的。還是以上面演示的實例來說,我們可以采用如下的方式在路由參數名后面添加一個問號(“?”),原本必需的路由參數變成了可以缺省的。可缺省的路由參數只能出現在路由模板尾部,這個應該不難理解。

   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”),它們都是完全等效的。

4

上面我們的程序相當于是在進行請求處理的時候給予了可缺省路由參數一個默認值,實際上路由參數默認值得設置還具有一種更簡單的方式,那就是按照如下所示的方式直接將默認值定義在路由模板中。如果采用這樣的路由注冊方式,我們針對WeatherForecast方法的改動就完全沒有必要了。

   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”)就可以獲取到某個城市指定日期的天氣。

5

對于上面設計的這個URL來說,我們采用“.”作為日期分隔符,如果我們采用“/”作為日期分隔符(比如“2016/11/11”),這個路由默認應該如何定義呢?由于“/”同時也是URL得路徑分隔符,如果表示日期的路由變量也采用相同的分隔符,意味著同一個路由參數跨越了多個路徑分段,我們只能定義“通配符”路由參數的形式來達到這個目的。通配符路由參數采用“{*variable}”這樣的形式,星號(“*”)表示路徑“余下的部分”,所以這樣的路由參數只能出現在模板的尾端。對我們的實例來說,路由模板可以定義成“/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: }

我們可以對程序做如上的修改來使用新的URL模板(“/weather/{city}/{*date}”)。這樣為了得到如上圖所示的北京在2016年11月11日的天氣,請求的URL可以替換成“/weather/010/2016/11/11”。


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]:內聯路由約束的檢驗

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()