ASP.NET Core路由中間件[1]: 終結點與URL的映射


借助路由系統提供的請求URL模式與對應終結點(Endpoint)之間的映射關系,我們可以將具有相同URL模式的請求分發給應用的終結點進行處理。ASP.NET Core的路由是通過EndpointRoutingMiddleware和EndpointMiddleware這兩個中間件協作完成的,它們在ASP.NET Core平台上具有舉足輕重的地位,因為ASP.NET Core MVC框架就建立在這個中間件之上。可以將一個ASP.NET Core應用視為一組終結點的組合,所謂的終結點可以理解為能夠通過HTTP請求的形式訪問的遠程服務。每個終結點通過RequestDelegate對象來處理路由過來的請求。ASP.NET Core的路由是通過EndpointRoutingMiddleware和EndpointMiddleware這兩個中間件來實現的,這兩個中間件類型都定義在NuGet包“Microsoft.AspNetCore.Routing”中。為了使讀者對實現在RouterMiddleware的路由功能有一個大體的認識,下面先演示幾個簡單的實例。[更多關於ASP.NET Core的文章請點這里]

目錄
一、路由注冊
二、設置內聯約束
三、默認路由參數
四、特殊的路由參數

一、路由注冊

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

15-1

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

public class WeatherReport
{
    private static string[] _conditions = new string[] { "", "多雲", "小雨" };
    private static Random _random = new Random();

    public string City { get; }
    public IDictionary<DateTime, WeatherInfo> WeatherInfos { get; }

    public WeatherReport(string city, int days)
    {
        City = city;
        WeatherInfos = new Dictionary<DateTime, WeatherInfo>();
        for (int i = 0; i < days; i++)
        {
            WeatherInfos[DateTime.Today.AddDays(i + 1)] = new WeatherInfo
            {
                Condition = _conditions[_random.Next(0, 2)],
                HighTemperature = _random.Next(20, 30),
                LowTemperature = _random.Next(10, 20)
            };
        }
    }

    public WeatherReport(string city, DateTime date)
    {
        City = city;
        WeatherInfos = new Dictionary<DateTime, WeatherInfo>
        {
            [date] = new WeatherInfo
            {
                Condition = _conditions[_random.Next(0, 2)],
                HighTemperature = _random.Next(20, 30),
                LowTemperature = _random.Next(10, 20)
            }
        };
    }

    public class WeatherInfo
    {
        public string Condition { get; set; }
        public double HighTemperature { get; set; }
        public double LowTemperature { get; set; }
    }
}

由於用於處理請求的處理器最終體現為一個RequestDelegate對象,所以我們定義了如下一個與這個委托類型具有一致聲明的WeatherForecast方法來處理對應的請求。如下面的代碼片段所示,我們在這個方法中直接調用HttpContext的GetRouteData擴展方法提取RoutingMiddleware中間件在路由解析過程中設置的路由參數。GetRouteData擴展方法返回的是一個具有字典結構的對象,它的Key和Value分別代表路由參數的名稱與值,通過預先定義的參數名(city和days)可以得到目標城市和預報天數。

public class Program
{
    private static Dictionary<string, string> _cities = new Dictionary<string, string>
    {
        ["010"] = "北京",
        ["028"] = "成都",
        ["0512"] = "蘇州"
    };

    public static async Task WeatherForecast(HttpContext context)
    {
        var city = (string)context.GetRouteData().Values["city"];
        city = _cities[city];
        int days = int.Parse(context.GetRouteData().Values["days"].ToString());
        var report = new WeatherReport(city, days);
        await RendWeatherAsync(context, report);
    }

    private static async Task RendWeatherAsync(HttpContext context, WeatherReport report)
    {
        context.Response.ContentType = "text/html;charset=utf-8";
        await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>");
        await context.Response.WriteAsync($"<h3>{report.city}</h3>");
        foreach (var it in report.WeatherInfos)
        {
            await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}:");
            await context.Response.WriteAsync($"{it.Value.Condition}({ it.Value.LowTemperature}℃ ~ { it.Value.HighTemperature}℃)< br />< br /> ");
        }
        await context.Response.WriteAsync("</body></html>");
    }    
    ...
}

有了這兩個核心參數之后,我們可以據此生成一個WeatherReport對象,並將它攜帶的天氣信息以一個HTML文檔的形式響應給客戶端,圖15-1就是這個HTML文檔在瀏覽器上的呈現效果。由於目標城市最初以電話區號的形式體現,所以在呈現天氣信息的過程中我們還會根據區號獲取具體城市的名稱。簡單起見,我們利用一個簡單的字典來維護區號和城市之間的關系,並且只存儲了3個城市而已。

下面完成所需的路由注冊工作。如下面的代碼片段所示,我們調用IApplicationBuilder的UseRouting方法和UseEndpoints方法分別完成針對EndpointRoutingMiddleware與EndpointMiddleware這兩個終結點的注冊。由於它們在進行路由解析過程中需要使用一些服務,所以可以調用IServiceCollection的AddRouting擴展方法來對它們進行注冊。

public class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(endpoints=> endpoints.MapGet("weather/{city}/{days}", WeatherForecast))))
            .Build()
            .Run();
    }
}

UseEndpoints方法提供了一個Action<IEndpointRouteBuilder>類型的參數,我們利用這個參數調用IEndpointRouteBuilder的MapGet方法提供了一個路由模板與對應處理器之間的映射。我們指定的路徑模板為“weather/{city}/{days}”,其中攜帶兩個路由參數({city}和{days}),分別代表獲取天氣預報的目標城市和天數。由於針對天氣請求的處理實現在WeatherForecast方法中,所以將指向這個方法的RequestDelegate對象作為第二個參數。MapGet的后綴“Get”表示HTTP方法,這意味着與指定路由模板的模式相匹配的GET請求才會被路由到WeatherForecast方法對應的終結點。

二、設置內聯約束

上面的演示實例注冊的路由模板中定義了兩個參數({city}和{days}),分別表示獲取天氣預報的目標城市對應的區號和天數。區號應該具有一定的格式(以零開始的3~4位數字),而天數除了必須是一個整數,還應該具有一定的范圍。由於我們在注冊的時候並沒有為這個兩個路由參數的值做任何約束,所以請求URL攜帶的任何字符都是有效的。而處理請求的WeatherForecast方法也並沒有對提取的數據做任何驗證,所以在執行過程中面對不合法的輸入會直接拋出異常。如下圖所示,由於請求URL(“/weather/0512/iv”)指定的天數不合法,所以客戶端接收到一個狀態為“500 Internal Server Error”的響應。

15-2

為了確保路由參數值的有效性,在進行路由注冊時可以采用內聯(Inline)的方式直接將相應的約束規則定義在路由模板中。ASP.NET Core為常用的驗證規則定義了相應的約束表達式,我們可以根據需要為某個路由參數指定一個或者多個約束表達式。如下面的代碼片段所示,為了確保URL攜帶的是合法的區號,我們為路由參數{city}指定了一個針對正則表達式的約束(:regex(^0[1-9]{{2,3}}$))。由於路由模板在被解析時會將{value}這樣的字符理解為路由參數,如果約束表達式需要使用字符“{}”(如正則表達式^0[1-9]{2,3}$)),就需要采用“{{}}”進行轉義。而路由參數{days}則應用了兩個約束:第一個是針對數據類型的約束(:int),它要求參數值必須是一個整數;第二個是針對區間的約束(:range(1,4)),意味着我們的應用最多只提供未來4天的天氣。

public class Program
{
    public static void Main()
    {
        var template = @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}";
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
            .Build()
            .Run();    
    }
    ...
}

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

15-3

三、默認路由參數

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

public class Program
{    
    public static void Main()
    {
        var template = "weather/{city?}/{days?}";
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
            .Build()
            .Run();
    }
    ...
}

既然路由變量占據的部分路徑是可以默認的,那么即使請求的URL不具有對應的內容(如“weather”和“weather/010”),它與路由規則也是匹配的,但此時在路由參數字典中是找不到它們的。由於表示目標城市和預測天數的兩個路由參數都是默認的,所以需要對處理請求的WeatherForecast方法做相應的改動。下面的代碼片段表明:如果請求URL為了顯式提供對應參數的數據,那么它們的默認值分別為010(北京)和4(天),也就是說,應用默認提供北京未來4天的天氣。

public class Program
{    
    public static async Task WeatherForecast(HttpContext context)
    {
        var routeValues = context.GetRouteData().Values;
        var city = routeValues.TryGetValue("city", out var v1)
            ? (string)v1
            : "010";
        city = _cities[city];
        var days = routeValues.TryGetValue("days", out var v2)
            ? int.Parse(v2.ToString())
            : 4;          
        var report = new WeatherReport(city, days); 
        await RendWeatherAsync(context, report);
    }
    ...
}

針對上述改動,如果希望獲取北京未來4天的天氣狀況,我們可以采用下圖所示的3種URL(“weather”、“weather/010”和“weather/010/4”),它們是完全等效的。

15-4

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

public class Program
{
    public static void Main()
    {
        var template = "weather/{city=010}/{days=4}";
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
            .Build()
            .Run();
    }
    ...
}

四、特殊的路由參數

一個URL可以通過分隔符“/”划分為多個路徑分段(Segment),路由模板中定義的路由參數一般來說會占據某個獨立的分段(如“weather/{city}/{days}”)。但也有例外情況,我們既可以在一個單獨的路徑分段中定義多個路由參數,也可以讓一個路由參數跨越多個連續的路徑分段。

下面先介紹在一個獨立的路徑分段中定義多個路由參數的情況。同樣以前面演示的獲取天氣預報的路徑為例,假設設計一種路徑模式來獲取某個城市某一天的天氣信息,如“/weather/010/2019.11.11”這樣一個URL可以獲取北京在2019年11月11日的天氣,那么路由模板為“/weather/{city}/{year}.{month}.{day}”。

public class Program
{
    public static void Main()
    {
        var template = "weather/{city}/{year}.{month}.{day}";
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
            .ConfigureServices(svcs => svcs.AddRouting())
            .Configure(app => app.UseRouter(builder => builder.MapGet(template, WeatherForecast))))
            .Build()
            .Run();
    }

    public static async Task WeatherForecast(HttpContext context)
    {
        var values = context.GetRouteData().Values;
        var city = values["city"].ToString();
        city = _cities[city];
        int year = int.Parse(values["year"].ToString());
        int month = int.Parse(values["month"].ToString());
        int day = int.Parse(values["day"].ToString());
        var report = new WeatherReport(city, new DateTime(year, month, day));
        await RendWeatherAsync(context, report);
    }
    ...
}

由於URL采用了新的設計,所以我們按照如上形式對相關程序進行了相應的修改。現在我們采用“/weather/{city}/{yyyy}.{mm}.{dd}”這樣的URL,就可以獲取某個城市指定日期的天氣。如下圖所示,我們采用請求路徑“/weather/010/2019.11.11”可以獲取北京在2019年11月11日的天氣。

15-5

對於上面設計的這個URL來說,我們采用“.”作為日期分隔符,如果采用“/”作為日期分隔符(如2019/11/11),這個路由默認應該如何定義?由於“/”也是路徑分隔符,如果表示日期的路由變量也采用相同的分隔符,就意味着同一個路由參數跨越了多個路徑分段,我們只能采用定義“通配符”的形式來達到這個目的。通配符路由參數采用{*variable}或者{**variable}的形式,星號(*)表示路徑“余下的部分”,所以這樣的路由參數只能出現在模板的尾端。對我們的實例來說,路由模板可以定義成“/weather/{city}/{*date}”。

public class Program
{
    public static void Main()
    {
        var template = "weather/{city}/{*date}";
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseRouting()
                    .UseEndpoints(routes => routes.MapGet(template, WeatherForecast))))
            .Build()
            .Run();
    }

    public static async Task WeatherForecast(HttpContext context)
    {
        var values = context.GetRouteData().Values;
        var city = values["city"].ToString();
        city = _cities[city];
        var date = DateTime.ParseExact(values["date"].ToString(), "yyyy/MM/dd", CultureInfo.InvariantCulture);
        var report = new WeatherReport(city, date);
        await RendWeatherAsync(context, report);
    }
    ...
}

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

15-6

ASP.NET Core路由中間件[1]: 終結點與URL的映射
ASP.NET Core路由中間件[2]: 路由模式
ASP.NET Core路由中間件[3]: 終結點
ASP.NET Core路由中間件[4]: EndpointRoutingMiddleware和EndpointMiddleware
ASP.NET Core路由中間件[5]: 路由約束


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM