ASP.NET Core 中文文檔 第三章 原理(4)路由


原文:Routing
作者:Ryan NowakSteve SmithRick Anderson
翻譯:張仁建(Stoneqiu)
校對:許登洋(Seay)謝煬(kiler398)孟帥洋(書緣)姚阿勇(Mr.Yao)

路由是用來把請求映射到路由處理程序。應用程序一啟動就配置了路由,並且可以從URL中提取值用於處理請求。它還負責使用 ASP.NET 應用程序中定義的路由來生成鏈接。

這份文檔涵蓋了初級的ASP.NET核心路由。對於 ASP.NET 核心 MVC 路由, 請查看 Routing to Controller Actions

章節:

查看或下載示例代碼

路由基礎

路由使用 routes 類 ( IRouter 的實現) 做到:

  • 映射傳入的請求到 路由處理程序
  • 生成響應中使用的 URLs

一般來說,一個應用會有單個路由集合。這個集合會按順序處理。請求會在這個路由集合里按照 URL matching 來查找匹配。響應使用路由生成 URLs。

路由通過 RouterMiddleware 類連接到 middleware 管道。 作為配置的一部分 ASP.NET MVC 可以增加路由到中間件管道。 要了解如何使用路由作為獨立組件, 請看 using-routing-middleware

URL 匹配

路由匹配指的是路由調度請求到一個處理程序的過程。這個過程通常是基於URL路徑中的數據,但可以擴展到請求中的任何數據。調度請求到不同處理程序的能力是應用調節自身大小和復雜度的關鍵。

請求調用序列中每個路由的異步方法來進入路由中間件IRouter 實例通過設置 RouteContext Handler 為一個不為空的 RequestDelegate 來選擇是否處理請求。如果一個處理程序已經設置了路由,那么它就將被調用來處理這個請求,並且不會有其他的路由再去處理。如果所有的路由都執行了,請求還沒有找到處理程序,那么中間件就會調用next方法,從而下一個在請求管道中的中間件就被調用了。

RouteAsync 的主要輸入是和當前請求關聯的 RouteContext HttpContext 。在一個成功匹配之后, RouteContext.HandlerRouteContext RouteData 會作為輸出。

RouteAsync 執行期間,一個成功匹配會基於已經完成的請求處理設置 RouteContext.RouteData 的屬性為合適的值。當一個路由成功匹配了一個請求時,RouteContext.RouteData 包含了重要的關於匹配結果的狀態信息。

RouteData Values 是一個從路由產生的 路由值 字典。這些值通常由標記化的 URL 確定的,可以用來接收用戶輸入,或者用來在應用內部做更深層的調度決定。

RouteData DataTokens 是相關的匹配路由附加數據的屬性包。提供 數據令牌 支持與每個路由相關的狀態數據,這樣應用基於匹配的路由可以遲點做出決定。這些數據是開發人員定義的,不會式影響路由的行為。而且,數據令牌中的值可以是任何類型,對比路由值,它可以很容易的轉成字符串。

RouteData Routers 是一個成功匹配請求的路由列表。路由可以彼此嵌套,而且 Routers 屬性反映了請求通過路由邏輯樹導致匹配的路徑。一般來說, Routers 中的第一項就是一個路由集合,而且應該用來生成URL。 Routers 中的最后一項就是已匹配路由。

URL 生成

URL 生成是指的路由基於一系列的路由值創建一個URL路徑的過程。這允許你的處理程序和能訪問它們的URL直接有一個邏輯分離。

路由生成遵循一個類似的迭代過程,但開始於用戶或框架代碼調用到路由集合的 GetVirtualPath 方法時。每個路由的 GetVirtualPath 方法都會被調用,直到返回一個不為空的 VirtualPathData

GetVirtualPath 的主要輸入是:

路由主要使用 ValuesAmbientValues 提供的路由值來決定在哪兒生成一個 URL 以及包含什么值。 AmbientValues 是隨着路由系統匹配當前請求而產生的一系列路由值。 相反,Values 是用於指定如何生成當前操作所需的URL的路由值。提供 HttpContext 是以防路由需要獲取服務或當前上下文相關的數據。

建議
Values 看做是對 AmbientValues 的重載。URL生成嘗試重用來自當前請求的路由值,以便使用相同路由或路由值的鏈接更容易生成 URL。

GetVirtualPath 的輸出是一個 VirtualPathDataVirtualPathData是一個並行的 RouteData ;它包含了輸出 URL 的虛擬路徑以及應該由路由設置的一些額外的屬性。

VirtualPathData VirtualPath 屬性包含了路由生成的虛擬路徑。根據你的需求,可能需要進一步處理的。例如,如果你想在 HTML 中呈現生成的 URL,你需要預先設置好應用的基礎路徑。

VirtualPathData Router 是一個成功生成URL路由的參考。

VirtualPathData DataTokens 屬性是一個關聯到生成URL的路由的附加數據的字典集合,這個和 RouteData.DataTokens 是並行的。

創建路由

路由提供了 Route 類作為 IRouter 的標准實現。當調用 RouteAsync 方法時, Route 使用 路由模板 語法定義匹配URL路徑的模式。當調用 GetVirtualPath 方法時,Route會使用相同的路由模板生成 URL。

大多數的應用會通過調用 MapRoute 方法或者定義在 IRouteBuilder 接口上的一個類似的擴展方法來創建路由。所有的這些方法會創建一個 Route 實例並添加到路由集合中。

注意
MapRoute 不需要路由處理參數--它只添加將被 DefaultHandler 處理的路由。由於默認處理程序是一個 IRouter 對象,可能決定不去處理請求。MVC通常配置了一個默認處理程序,它只處理能匹配到可用的控制器和操作的請求。了解更多關於MVC的路由,請點擊 🔧 Routing to Controller Actions

這是一個典型的ASP.NET MVC路由調用 MapRoute 的例子

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

這個模板會匹配像 /Products/Details/17 這樣的URL路徑,而且提取的路由值是 { controller = Products, action = Details, id = 17 } 。這個路由值由幾個 URL 段組成,通過路由參數名稱匹配路由模板中的每一段。路由參數被命名,它們是由大括號 { } 中的參數名定義的。

上面的這個模板也能匹配URL路徑 /,而且還將產生值為 { controller = Home, action = Index } 。這是因為 {controller}{action} 都定義了默認值,而且 id 是可選的。路由參數名等號 = 后面的值是作為該參數的默認值。而參數名后面的問號 ? 標記着這個參數是可選的。帶有默認參數的路由值 always 是會在路由匹配的時候產生一個路由值 - 而如果沒有相應的 URL 路徑段,可選參數將不會產生一個路由值。

查看路由模板特性和語法的完整描述請移步 route-template-reference

這是一個 路由約束 的示例:

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id:int}");

這個模板將只會匹配像 /Products/Details/17 這樣的URL路徑,而不會匹配 /Products/Details/Apples 這樣的。 {id:int}id 這個路由參數定義了一個 路由約束 。路由約束實現自 IRouteConstraint 接口,它會檢查並驗證路由值。在這個例子中 id 必須可以轉換為一個整形。更多詳情請看 route-constraint-reference

另外,MapRoute 的重載接受 約束, 數據令牌, 和 默認值 。這些額外的參數都被定義為 object類型。這些參數的典型用法是傳遞一個匿名類型的對象,其中匿名類型的屬性名必須匹配路由參數的名稱。

下面這兩個例子創建的路由是等效的:

routes.MapRoute(
    name: "default_route",
    template: "{controller}/{action}/{id?}",
    defaults: new { controller = "Home", action = "Index" });

routes.MapRoute(
    name: "default_route",
    template: "{controller=Home}/{action=Index}/{id?}");

建議
對應簡單的路由使用內嵌語法定義約束和默認值可能更方便些。然而,有些特性比如數據令牌嵌入式語法是不支持的。

這個例子展示了幾個特點:

routes.MapRoute(
  name: "blog",
  template: "Blog/{*article}",
  defaults: new { controller = "Blog", action = "ReadArticle" });

這個模板會匹配 /Blog/All-About-Routing/Introduction 這樣的URL路徑而且會提取出 { controller = Blog, action = ReadArticle, article = All-About-Routing/Introduction } 值。controlleraction的默認值是由路由產生,即使模板沒有相應的路由參數。默認值可以在路由模板中指定。通過在路由參數名稱前面增加一個 * 號, article 這個路由參數定義為一個 全部捕獲 型 。 全部捕獲型路由參數捕獲剩下的URL路徑,而且也能匹配空字符串。

增加路由約束和數據令牌的示例:

routes.MapRoute(
    name: "us_english_products",
    template: "en-US/Products/{id}",
    defaults: new { controller = "Products", action = "Details" },
    constraints: new { id = new IntRouteConstraint() },
    dataTokens: new { locale = "en-US" });

這個模板會匹配像 /en-US/Products/5 這樣的URL路徑而且會提取出值為 { controller = Products, action = Details, id = 5 } 和數據令牌 { locale = en-US }

URL 生成

Route類也可以通過結合一組路由值和路由模板來生成 URL。這在邏輯上是匹配URL路徑的逆過程。

建議
為了更好的理解URL生成,想象一下你要生成的URL然后考慮一下一個路由模板該如何去匹配那個URL。會產生什么值?這和 Route 類中 URL 是怎么樣生成的是一個意思。

本例使用了一個基本的 ASP.NET MVC 風格的路由:

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

根據路由值 { controller = Products, action = List } ,這個路由會生成URL /Products/List。路由值被相應的路由參數替換以形成URL路徑。因為 id 是一個可選的路由參數,所以它沒有值也沒問題。

根據路由值 { controller = Home, action = Index } ,這個路由會生成 URL / 。因為提供的路由值匹配了默認值,因此可以安全地忽略相應的片段。注意到這兩個URL生成在路由定義和產生用於生成 URL 的路由值之間是雙向的。

建議
在ASP.NET MVC應用中應該使用 UrlHelper 類生成 URL,而不是直接調用路由。

更多關於 URL 生成過程的細節可以點擊 url-generation-reference

使用路由中間件

在使用路由前,需要將其添加到 project.json依賴性 中:

"Microsoft.AspNetCore.Routing": <current version>

Startup.cs 中添加路由到服務容器

public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
}

路由必須在 Startup 類的 Configure 方法中配置。下面的例子使用了這些APIs:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    var trackPackageRouteHandler = new RouteHandler(context =>
    {
        var routeValues = context.GetRouteData().Values;
        return context.Response.WriteAsync(
            $"Hello! Route values: {string.Join(", ", routeValues)}");
    });

    var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);

    routeBuilder.MapRoute(
        "Track Package Route",
        "package/{operation:regex(^track|create|detonate$)}/{id:int}");

    routeBuilder.MapGet("hello/{name}", context =>
    {
        var name = context.GetRouteValue("name");
        // This is the route handler when HTTP GET "hello/<anything>"  matches
        // To match HTTP GET "hello/<anything>/<anything>, 
        // use routeBuilder.MapGet("hello/{*name}"
        return context.Response.WriteAsync($"Hi, {name}!");
    });            

    var routes = routeBuilder.Build();
    app.UseRouter(routes);

已給URLs的響應列表。

URI Response
/package/create/3 Hello! Route values: [operation, create], [id, 3]
/package/track/-3 Hello! Route values: [operation, track], [id, -3]
/package/track/-3/ Hello! Route values: [operation, track], [id, -3]
/package/track/ <未通過,沒有匹配>
GET /hello/Joe Hi, Joe!
POST /hello/Joe <未通過, 只匹配GET請求>
GET /hello/Joe/Smith <未通過,沒有匹配>

如果你只配置了一個路由,調用 app.UseRouter 方法,並傳遞一個IRouter實例。而不必調用 RouteBuilder

框架提供了一系列的創建路由的擴展方法,比如:

一些像 MapGet 這樣的方法,需要提供一個 RequestDelegate請求委托 在路由匹配的時候將被用做 路由處理程序 。這個系列的其他方法運行配置一個中間件管道來用做路由處理程序。如果 Map 方法沒有接受到一個處理程序,例如 MapRoute ,那么它會調用 DefaultHandler

Map[Verb] 方法在方法名中使用約束限制路由的請求方式。例如 MapGetMapVerb

路由模板參考

令牌內的大括號( { } )定義了路由值 參數的邊界。你可以在一個路由段中定義多個路由值參數,但它們必須用文字值分開,例如 {controller=Home}{action=Index} 不是一個有效路由,因為在 {controller}{action} 之間沒有文字值。這些路由值參數必須有一個名稱,並可以有附加指定的屬性。

除了路由參數(例如, {id} )和路徑符合 / 之外的文本必須匹配URL中的值。文本匹配是大小寫敏感的且基於解碼后的URLs路徑。如果要匹配路由參數分隔符 {} ,重復( {{}} )即可。

試圖捕獲一個帶有可選擴展名的文件名的 URL 模式還需要有其他考慮。例如,使用模板 files/{filename}.{ext?} - 當文件名和擴展名都存在的時候,這兩個值會被填充。如果在URL中只存在文件名,因為點號 . 是可選的路由也會匹配。下面的 URLs 將會匹配這個路由。

  • /files/myFile.txt
  • /files/myFile.
  • /files/myFile

你可以使用 * 號作為一個路由參數的前綴去綁定其余的 URI - 這被叫做全捕獲參數。例如, blog/{*slug} 將會匹配任何以 /blog 開頭且有任何值跟隨(對應到 slug 路由值)的 URI。全捕獲型參數也能匹配空字符串。

路由參數可以有默認值,定義的方式是在參數名稱后定義默認值,用 = 號分開。例如, {controller=Home}Home 作為 controller 的默認值。如果在URL中這個參數沒有值就將使用默認值。此外對於默認值,路由參數是可選的(通過在參數名稱后定義加一個 ? ,比如 id? )。在參數可選和擁有默認值之間的區別就是擁有默認值的路由參數總是會參數一個值;而可選的參數只有URL提供值了才會有值。

路由參數也可以有約束,必須匹配從URL綁定的路由值。通過在路由參數名后增加一個冒號 : 和約束名來定義一個內聯約束。如果內聯約束需要參數,需要在約束名稱后用括號括起來提供。多個內聯約束可以通過附加另一個冒號 : 和約束名來定義。為在URL處理中使用 IRouteConstraint 實例,需要把約束名稱傳遞到 IInlineConstraintResolver 服務來創建一個。
下面的列表展示了一些路由模板和他們的行為。

路由模板 匹配URL示例 注釋
hello /hello 將只匹配'/hello' 路徑
{Page=Home} / 將匹配且設置 PageHome
{Page=Home} /Contact 將匹配且設置 PageContact
{controller}/{action}/{id?} /Products/List 會映射到 Products 控制器的 List 方法。
{controller}/{action}/{id?} /Products/Details/123 會映射到 Products控制器的 Details方法,且 id 的值為 123
{controller=Home}/ {action=Index}/{id?} / 會映射到 Home 控制器的 Index 方法; id 忽略掉。

大體來說,設置路由的最簡單的方法是使用模板。約束和默認值可以在路由模板之外定義。

建議
打開 logging 去查看內置路由的實現方式, 例如 Route 類, 如何匹配請求。

路由約束參考

當一個路由已經匹配到了傳入的URL並標記了URL中的路由值時路由約束就會執行。它一般會檢查和路由模板相關的路由的值而且會做出一個關於這個值是否是可接受的簡單決定。有些路由約束使用路由值之外的數據來決定該請求是否可以進行路由。例如, HttpMethodRouteConstraint 可以接受或拒絕一個基於其Hppt的請求。

警告
避免使用約束來做驗證,這樣做意味着非法輸入會得到一個404(沒有找到)的結果。而不是一個400的狀態碼加上合適的錯誤信息。路由約束應該用來在路由間做區分,而不是為了特定的路由做驗證。

下表展示了一些路由約束和他們的預期行為。

約束 示例 匹配示例 注釋
int {id:int} 123 匹配所有整型
bool {active:bool} true 匹配 truefalse
datetime {dob:datetime} 2016-01-01 匹配一個合法的 DateTime 值 (固定區域性 - 請看 options)
decimal {price:decimal} 49.99 匹配一個合法的 decimal
double {weight:double} 4.234 匹配一個合法的 double
float {weight:float} 3.14 匹配一個合法的 float
guid {id:guid} 7342570B- 匹配一個合法的 Guid
long {ticks:long} 123456789 匹配一個合法的 long
minlength(value) {username:minlength(5)} steve 至少5個字符串長。
maxlength(value) {filename:maxlength(8)} somefile 字符串不能超過8個字符長。
length(min,max) {filename:length(4,16)} Somefile.txt 字符串至少8個長度且不超過16個字符長度。
min(value) {age:min(18)} 19 值至少是18。
max(value) {age:max(120)} 91 值不能超過120。
range(min,max) {age:range(18,120)} 91 值必須介於18和120之間。
alpha {name:alpha} Steve 字符串必須是由字母字符組成。
regex(expression) {ssn:regex(^d{3}-d{2}-d{4}$)} 123-45-6789 字符串必須匹配提供的正則表達式。
required {name:required} Steve 用於在URL生成時強制必須存在值。

注意
驗證URL可轉換為CLR類型(例如int或DateTime)的路由約束總是使用固定區域性 - 它們認為URL是不可本地化的。框架提供的路由約束不會修改路由值。從URL解析過來的所有路由值都會存為字符串。例如,浮點路由約束 會試圖將路由值轉換為一個浮點型,但轉換后的值只用於驗證它是否能夠轉換為浮點型。

建議
為約束一個參數是一組列可能的值,你可以使用正則表達式(例如 {action:regex(list|get|create)} )。這將只匹配 action 的值是 listgetcreate 。 如果將 "list|get|create" 傳入約束字典,是等價的。傳入約束字典的約束(沒有內聯模板),沒有匹配到已知的約束也會被視為正則表達式。

URL生成參考

下面的例子展示了在給定一個路由值字典和一個 路由集合 的情況下如何生成一個鏈接。

app.Run(async (context) =>
{
    var dictionary = new RouteValueDictionary
    {
        { "operation", "create" },
        { "id", 123}
    };

    var vpc = new VirtualPathContext(context, null, dictionary, "Track Package Route");
    var path = routes.GetVirtualPath(vpc).VirtualPath;

    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync("Menu<hr/>");
    await context.Response.WriteAsync($"<a href='{path}'>Create Package 123</a><br/>");
});

上面示例最終生成的 相對路徑/package/create/123

VirtualPathContext 構造函數的第二個參數是一個 ambient值 的集合。通過限制開發人員必須在特定請求上下文中定義值的數量,環境值提供了方便。當前請求的路由值被當做生成鏈接的環境值。例如,在MVC應用中,如果正在Home控制器的About方法中,鏈接到Index方法時你不需要定義控制器路由值(Home的環境值將會被使用)。

沒有匹配到參數的環境值將被忽略,同樣有明確保留的值覆蓋它,按照URL中從做到右的順序,環境值也會被忽略。

顯示提供的但沒有匹配的值將會被加到查詢字符串中。下表展示了使用路由模板 {controller}/{action}/{id?} 的結果

環境值 明確值 結果
controller="Home" action="About" /Home/About
controller="Home" controller="Order",action="About" /Order/About
controller="Home",color="Red" action="About" /Home/About
controller="Home" action="About",color="Red" /Home/About?color=Red

如果一個路由有一個沒有匹配到參數的默認值,而且這個值被提供了,那它必須匹配這個默認值。例如:

routes.MapRoute("blog_route", "blog/{*slug}",
  defaults: new { controller = "Blog", action = "ReadPost" });

當提供了controller和action的匹配值,才能生成這個路由的鏈接。

返回目錄


免責聲明!

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



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