翻譯 - 基本知識 - ASP.NET Core 路由

翻譯自:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0ios

路由負責匹配 Http 請求,而後分發這些請求到應用程序的最終執行點。Endpoints 是應用程序可執行請求處理的代碼單元。Endpoints 在應用程序中定義並在應用程序啓動的時候配置。git

Endpoint 匹配處理能夠從請求的 URL 中提出值和爲請求處理提供值。使用從應用程序獲取的 Endpoint 信息,路由也能夠生成匹配 Endpoint 的 URLS。github

 

應用能夠經過如下方式配置路由:正則表達式

  • 控制器
  • Razor Pages
  • SignalR
  • gPRC Services
  • Endpoint - enable 中間件,例如: Health Checks.
  • 使用路由註冊的代理和 lambdas.

這篇文檔涵蓋了ASP.NET Core 路由的底層詳情。算法

這篇文檔中描述的 Endpoint 路由系統適用於 ASP.NET Core 3.0 或者更新的版本。數據庫

路由基礎express

全部的 ASP.NET Core 模板代碼中都包含路由。路由在 Startup.Configure 中註冊在中間件管道中。編程

下面代碼展現了一個路由的示例:json

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

路由使用了一對中間件,經過 UseRouting 和 UseEndPoints 註冊:api

  • UseRouting 添加路由匹配到中間件管道中。這個路由匹配中間件查詢在應用程序中定義的 Endpoints 的集合,選擇最佳匹配請求的 Endpoint。
  • UseEndpoints 添加 Endpoint 的執行體到中間件管道。它經過關聯到選擇的 Endpoint 代理執行。

前面這個示例包含了一個單獨的路由到代碼的 Endpoint 使用 MapGet 方法:

  • 當一個 HTTP GET 請求發送到根 URL /:
    - 請求代理展現執行
    - Hello World! 被寫入 HTTP 請求迴應中。默認的,根 URL / 是 https://localhost:5001/。
  • 若是說請求方法不是 GET 或者 根 URL 不是 /,沒有路由匹配的狀況下會返回 HTTP 404。

 Endpoint

MapGet 方法用來定義一個 Endpoint。一個 endpoint 能夠是如下狀況:

  • 選擇:經過匹配 URL 和 HTTP 方法
  • 執行:經過執行代理

在 UseEndpoints 中配置的 Endpoints 能夠被 APP 匹配和執行。例如,MapGet, MapPost, 和一些相似於鏈接請求代理到路由系統的方法。更多的方法能夠被用於鏈接 ASP.NET Core 框架的特性到路由系統中:

  • MapRazorPages 用於 Razor Pages
  • MapController 用於 控制器
  • MapHub<THub> 用戶 SignalR
  • MapGrpcService<TService> 用於 gPRC

下面這個例子展現了一個路由一個比較複雜的路由模板:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});

字符串 /hello/{name:alpha} 是一個路有模板。被用來配置 endpoint 如何被匹配到。在這個例子中,模板匹配如下狀況:

  • 像 /hello/Ryan 這樣一個 URL
  • 任何的以 /hello/ 開頭的,緊跟一串字母的 URL。:alpha 應用了一個路由約束,它僅僅匹配字母。路由約束會在下面介紹到。

{name:alpha}: 上面 URL 路徑中的第二段:

  • 被綁定到 name 參數上
  • 被捕獲並存儲到 HttpRequest.RouteValues 中

 當前文檔中描述的 endpoint 路由系統是在 ASP.NET Core 3.0 中新添加的。然而,全部版本的 ASP.NET Core 都支持一樣的路由模板特性和路由約束的集合。

下面的示例展現了帶有 health checks 和 受權的路由:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

上面這個示例展現瞭如何:

  • 受權中間件能夠被路由使用
  • Endpoints 能夠被用於配置受權行爲

MapHealthChecks 添加了一個 health check endpoint。接着又調用了 RequireAuthorization 附加了一個受權策略到這個 endpoint 上。

UseAuthentication 和 UseAuthorization 添加認證和受權中間件。這些中間件在 UseRouting 和 UseEndpoints 中間調用,所以能夠:

  • 查看哪一個 endpoint 被選中經過 UseRouting
  • 在 UseEndpoints 分發請求到 endpoint 以前應用受權策略

Endpoint 元信息

在前面這個例子中,有兩個 endpoints,可是隻有 health check 附加了一個受權策略。若是請求匹配了 health check, /healthz,受權檢查就會被執行。這說明 endpoints 能夠有額外的數據附加到他們上面。這寫額外的數據叫作 endpoint metadata:

  • metadata 能夠被路由中間件處理
  • metadata 能夠是任何的 .NET 類型

路由的概念

路由系統經過添增強大的 endpoint 概念創建在中間件管道之上。Endpoints 表明了一組應用程序的功能,這些功能和路由,受權和 ASP.NET Core 核心系統功能是不一樣的。

ASP.ENT Core 中 endpoint 的定義

ASP.NET Core endpoint:

  • 可執行的:包含一個請求代理
  • 可擴展的:包含一個 Meatadata 集合
  • 可選擇的:可選的,包含路由信息
  • 可枚舉的:enpoint 的集合能夠經過 EndpointDataSource 獲取被列出來

下面的代碼展現瞭如何獲取和檢查匹配當前請求的 endpoint:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.Use(next => context =>
    {
        var endpoint = context.GetEndpoint();
        if (endpoint is null)
        {
            return Task.CompletedTask;
        }
        
        Console.WriteLine($"Endpoint: {endpoint.DisplayName}");

        if (endpoint is RouteEndpoint routeEndpoint)
        {
            Console.WriteLine("Endpoint has route pattern: " +
                routeEndpoint.RoutePattern.RawText);
        }

        foreach (var metadata in endpoint.Metadata)
        {
            Console.WriteLine($"Endpoint has metadata: {metadata}");
        }

        return Task.CompletedTask;
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

若是 endpoint 被選中了,那麼能夠從 HttpContext 中獲取到。它的屬性能夠被檢測到。Endpoint 對象是不可變的,建立以後就不能夠修改了。最多見的 endpoint 類型是 RouteEnpoint。RouteEndpoint 包含了它能夠被路由系統選擇的信息。

在前面的代碼中,app.Use 配置了一個行內的 middleware。

下面的代碼展現了因爲 app.Use 調用位置不一樣,可能就沒有一個 enpoint。

// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // Location 3: runs when this endpoint matches
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello");
});

// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

上面示例添加了 Console.WriteLine 語句,顯示了是否一個 endpoint 被選中。爲了清晰,示例中爲 / endpoint 增長了名稱顯示。

運行這段代碼,訪問 /,將會顯示:

1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello

若是訪問其餘 URL,則會顯示:

1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)

輸出結果說明了:

  • 在 UseRouting 調用以前,enpoint 老是 null
  • 在 UseRouting 和 UseEndpoints 之間,若是一個匹配被發現,endpoin 就不是 null
  • 當一個匹配被發現時,UseEndpoints 中間件會是一個終結。終結中間件在文檔後面會講到
  • 在 UseEndpoints 以後的中間件只有在沒有匹配被發現的時候纔會執行

UseRouting 中間件使用 SetEndpoint 方法把 endpoint 附加到當前請求上下文。可使用自定義的邏輯替換掉 UseRouting 而且使用 endpoint 好處。Endpoints 是和中間件相似的低級別的原語,不和路由的實現耦合在一塊兒。大多數的應用程序不須要自定義邏輯替換 UseRouting。

UseEndpoints 中間件被設計用來和 UseRouting 中間件配合使用。執行一個 endpoint 的核心邏輯並不複雜。 使用 GetEndpoint 獲取 endpoint,而後調用它的 RequestDelegate 屬性。

下面的代碼展現了中間件如何對路由產生影響或者作出反應:

public class IntegratedMiddlewareStartup
{ 
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Location 1: Before routing runs. Can influence request before routing runs.
        app.UseHttpMethodOverride();

        app.UseRouting();

        // Location 2: After routing runs. Middleware can match based on metadata.
        app.Use(next => context =>
        {
            var endpoint = context.GetEndpoint();
            if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
                                                                            == true)
            {
                Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
            }

            return next(context);
        });

        app.UseEndpoints(endpoints =>
        {         
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello world!");
            });

            // Using metadata to configure the audit policy.
            endpoints.MapGet("/sensitive", async context =>
            {
                await context.Response.WriteAsync("sensitive data");
            })
            .WithMetadata(new AuditPolicyAttribute(needsAudit: true));
        });

    } 
}

public class AuditPolicyAttribute : Attribute
{
    public AuditPolicyAttribute(bool needsAudit)
    {
        NeedsAudit = needsAudit;
    }

    public bool NeedsAudit { get; }
}

上面的示例展現了兩個重要的概念;

  • 中間件能夠在 UseRouting 以前運行,而後修改路由的運行行爲。一般的,這些出如今路由以前的中間件,會修改一些請求的屬性,好比 UseRewriter, UseHttpMethodOverride, 或者 UsePathBase。
  • 在中間件被執行以前,中間件能夠運行在 UseRouting 和 UseEndpoints 之間處理一些路由的結果。運行在 UseRouting 和 UseEndpoints 之間的中間件:一般會檢查 metadata 去理解 endpoints;一般會作出安全方面的決定,好比使用 UseAuthorization 和 UseCors。
  • 中間件和 metadata 的結合容許爲每個 endpoint 配置策略。

上面的代碼展現了一個自定義的支持爲每個 endpoint 添加策略的 endpoint。這個中間件輸出訪問敏感數據的 audit log 到控制檯。這個中間件可使用 AuditPolicyAttribute metadata 配置爲一個 audit enpoint。這個示例展現了一個選擇模式,只有 enpoints 被標記爲敏感的纔會被驗證。也能夠反向定義邏輯,例如驗證沒有被標記爲安全的一切。endpoint metadata 系統是靈活的。邏輯能夠被設計爲任何符合使用狀況的方式。

上面的示例代碼是爲了展現 endpoints 的基本概念。示例不是爲了用於生產環境。一個更完整的 audit log 中間件應該是這樣的:

  • 日誌保存到一個文件或者數據庫中
  • 包含詳細信息,例如用戶,IP地址,敏感 endpoint 的名稱以及更多的信息

audit 策略 metadata AuditPolicyAttribute 被定義爲一個 Attribute 是爲了在一個 class-based 的 framework 中更加容易使用,例如 controllers 和 SignalR。當使用路由編碼時:

  • Metadata 是附加到一個 builder API 上的
  • Class-based frameworks 包含建立 endpoints 時關於對應的方法和類的全部的屬性

對於 metadata 類型的最佳實踐是把它們定義爲接口或者屬性。接口和屬性容許代碼複用。metadata 系統是靈活的而且不強加任何限制。

比較終端中間件和路由

下面的代碼展現了使用中間件和使用路由的差異:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Approach 1: Writing a terminal middleware.
    app.Use(next => async context =>
    {
        if (context.Request.Path == "/")
        {
            await context.Response.WriteAsync("Hello terminal middleware!");
            return;
        }

        await next(context);
    });

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Approach 2: Using routing.
        endpoints.MapGet("/Movie", async context =>
        {
            await context.Response.WriteAsync("Hello routing!");
        });
    });
}

中間件的方式展現的方法1:一個終端中間件。被叫作終端中間件是由於它匹配了如下操做:

  • 前面例子中中間件匹配的操做是 Path = "/" ,路由匹配的操做是 Path = "/Movie"
  • 當一個匹配成功的時候,執行了一些功能而後返回了,而不是調用 next 執行下一個中間件

被叫作終端中間件是由於它終結了搜索,執行了一些操做,而後就返回了

比較一個終端中間件和路由:

  • 兩種途徑都容許終結處理管道:中間件經過返回語句而不是調用 next 方法終結管道;路由 Endpoints 老是結束運行
  • 終結中間件容許在管道的任意地方調用;Endpoints 執行的位置在 UseEnpoints 中
  • 終結中間件容許任意的的代碼去決定何時去中間件匹配:自定義的路由匹配代碼多是很難寫正確的;路由爲典型的應用程序提供了一個直接的解決方案。大部分的應用程序不須要自定義路由匹配的邏輯
  • Enpoints 接口帶有中間件,例如 UseAuthorizaton 和 UseCors: 經過調用 UseAuthorization 和 UseCors 使用一個終端中間件要求手動的實現受權系統

一個 endpoint 定義了:

  • 一個處理請求的代理
  • 一個任意 metadata 的集合。metadata 用來實現橫向的基於策略的考慮和配置到每個 endpoint

終結中間件能夠是一個有效的工具,可是要求:

  • 大量的代碼和測試
  • 手動的集成集成其它系統以得到更好的靈活性

在編寫一個終結中間件以前,應該優先考慮使用集成到路由

現有的集成了 Map 或者 MapWhen 的終結中間件一般能夠在一個路由中實現 endpoint。MapHealthChecks 展現了 router-ware 的模型:

  • 在 IEndpointRouteBuilder 接口上寫了一個擴展方法
  • 使用 CreateApplicationBuilder 建立一個嵌套的中間件管道
  • 把中間件附加到一個新的管道,在這個例子中使用了 UseHealthChecks
  • 編譯中間件管道到一個 RequestDelegate
  • 調用 Map 而後提供一個新的中間件管道
  • 從擴展方法中返回 Map 提供的編譯後的對象

下面的代碼展現了 MapHealthChecks 的使用:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

上面的代碼展現了爲何返回一個建立後的對象是重要的。返回一個建立的對象運行應用程序開發者去配置策略,例如 endpoint 受權策略。在這個例子中,health check 中間件沒有直接集成受權系統。

metadata 系統的建立是爲了響應擴展性做者在使用終結中間件時所遇到的問題。對於每個中間件,它是不肯定的,是爲了實現中間件本身帶有受權系統的集成。

URL matching

  • 是路由匹配進站請求到一個 endpoint 的過程
  • 基於 URL path 和 headers 中的數據
  • 能夠被擴展爲考慮請求中的任意數據

當一個路由中間件執行的時候,它設置一個 Endpoint 而且設置路由的值到一個從當前請求中獲取的一個 HttpContext 的請求特性中:

  • 調用 HttpContext.GetEndpoint 獲取一個 endpoint
  • HttpRequest.RouteValues 獲取到路由值得集合

運行在路由中間件以後的中間件能夠檢測 endpoint 後決定執行的操做。舉個例子,一個受權中間件能夠查詢 endpoint 的 metadata 集合實現受權策略。在請求處理管道中的全部中間件執行完畢後,被選擇的 endpoint 的代理被調用。

基於 enpoint 路由的路由系統負責全部的請求分發的決定。由於中間件應用策略是基於被選擇的 endpoint,這是很重要的:

  • 任何可能影響分發或者應用程序安全策略的決定均可以在路由系統中肯定

 警告:爲了向後兼容,當一個控制器或者 Razor 頁面 endpoint 代理被執行的時候,目前基於執行的請求處理 RouteContext.RouteData 被設置爲合適的值。

RouteContext 類型在之後的版本中將被標記爲廢棄的在:

  • 遷移 RouteData.Value 到 HttpRequest.RouteValues 中
  • 遷移 RaouteData.DataTokens 從 endpoint metadata 獲取 IDataTokensMetadata

URL 匹配操做在一個可配置的分段集合中。在每個分段中,輸出是匹配的集合。經過下一個分段,匹配的集合會逐步縮小。路由的實現不保證匹配 endpoints 的處理順序。全部可能的匹配一次就會處理。URL的匹配遵循如下順序。

ASP.NET Core:

  1. 處理 URL path 和 endpoints 的集合比較和它們的路由模板,蒐集全部匹配的
  2. 處理列表,移除不知足路由約束的匹配
  3. 處理列表,移除不知足 MatcherPolicy 示例集合的匹配
  4. 使用 EndpointSelector 從處理列表中作出最終決定

  enpoints 列表的優先級遵循如下原則:

  • RouteEndpoint.Order
  • 路由模板優先

全部匹配的 endpoints 在每一個階段直到 EndpointSelector 執行。 EnpointSelector 是最後一個階段。它從全部匹配的 endpoints 中選擇優先級最高的 endpoint 做爲最佳匹配。若是有一樣優先級的匹配,一個模糊匹配的異常將會拋出。

路由的優先級是基於一個更加具體的路由模板被計算出來被賦予一個更高的優先級。例如,比較一個兩個路由模板  /hello 和 /{message}

  • 兩個模板都匹配 URL path /hello
  • /hello 更加具體,所以有更高優先級

一般的,在實際中,路由優先級已經作了一個從各類各樣的 URL schemes 中選擇最佳匹配的很好的工做。僅僅在爲了不歧義的時候使用 Order。

因爲路由提供了各類各樣類型的擴展性,路由系統不太可能花費大量的時間去計算有歧義的路由。考慮一下像 /{message:alpha} 和 /{message:int} 這兩個路由模板:

  • alpha 約束只匹配字母字符
  • int 約束只匹配數字
  • 這兩個模板擁有相同的路由優先級,可是沒有一個單獨的 URL 是它們都匹配的
  • 若是路由系統在 startup 中報告了一個歧義錯誤,這將會阻止這種狀況的使用

警告:

UseEnpoints 中的操做的順序不會影響路由的行爲,但有一個例外。 MapControllerRoute 和 MapAreaRoute 自動的會基於它們被調用的順序賦值一個排序的值給它們的 enpoints。這模擬了控制器的長期行爲,而這些控制器沒有路由器提供和舊的路由實現相同的保證。

在舊的路由實現中,是能夠實現依賴路由處理順序的擴展。ASP.NET Core 以及更新的版本中的 endpoint 路由:

  • 沒有路由的概念
  • 不提供順序保證。全部的 endpoints 都一次處理。

路由模板的優先級和 enpoint 選擇順序

路由模板優先是基於如何具體化一個路由模板,並給它賦予一個值得系統。路由模板優先級:

  • 避免在大多數狀況下調整 enpoints 順序的必要
  • 嘗試匹配路由行爲的常識性指望

例如,模板 /Products/List 和 /Products/{id}。對於 URL path,系統將會認爲 /Products/List 比/Produts/{id} 更加匹配。這是由於字面值段 /List 被認爲比參數 /{id} 有更高的優先級。

優先級工做原理和路由模板如何定義相結合的詳情以下:

  • 擁有更多段的模板被認爲更加具體
  • 字面文字的段被認爲比一個參數的段更加具體
  • 帶有約束的參數的段被認爲比沒有約束的參數的段更加具體
  • 一個複雜的段被認爲和一個帶約束參數的段同樣具體
  • Catch-all 參數最不具體。查看路由模板引用中的 catch-all 更多的關於 catch-all 路由的重要信息

URL 生成概念 

URL 生成:

  • 基於一組路由值建立一個 URL path 的過程
  • 容許在 enpoints 和 URLs 之間進行邏輯分離

Endpoint 裸遊包含 LinkGenerator API。LInkGenerator 做爲一個單利服務從依賴注入中獲取。LinkGenerator API 能夠在正在執行的請求的上下文以外執行。Mvc.IUrlHelper 和 scenarios 依賴於 IUrlHelper,例如 Tag Helpers,HTML  Helpers,以及 Action Results,在內部使用 LinkGenerator API 提供生成連接的功能。

路由生成器由地址和地址架構的概念的支持。一個地址架構是一種決定哪些 endpoints 應該被用來生成連接的方式。例如,許多用戶熟悉的從控制器獲取路由名稱和路由值以及 Razor Pages 被用來實現做爲一種地址架構。

連接生成器能夠連接到控制器和 Razor Pages 經過如下擴展方法:

  • GetPathByAction
  • GetUriByAction
  • GetPathByPage
  • GetUriByPage

這些方法的重載的參數包含 HttpContext。這些方法在功能上等同於 Url.Action 和 Url.Page,可是提供更多的靈活性和選擇。

GetPath* 之類的方法和 Url.Action 以及 Url.Page 很類似,它們生成的 URI 包含一個絕對路徑。GetUrl* 方法老是生成一個包含一個架構和主機的絕對 URI。接受參數 HttpContext 參數的方法在正在執行的請求的 Context 中生成一個 URI。除非重寫,不然路由值將使用當前正在執行的請求中的 URI base path,架構以及主機。

LinkGenerator 被地址調用。生成一個 URI 在如下兩個步驟中出現:

  1. 地址被綁定到一組匹配當前地址的 enpoints
  2. 直到一個路由模型匹配了提供的值被發現了,endpoint 的路由模型纔會被評估。輸出的結果組合了其它 URI 的部分並提供給連接生成器返回。

LinkGenerator 提供的方法支持生成任何類型的標準的連接的能力。使用 link generator 最方便的方式是經過那些爲特定地址類型操做的擴展方法:

GetPathByAddress 基於提供的值生成一個絕對路徑的 URI

GetUriByAdderss    基於提供的值生成一個絕對的 URI

⚠️ 警告:

注意調用 LinkGenerator 會有如下影響:

  • 在配置不驗證 Host headers 的應用程序中,請謹慎使用 GetUri* 擴展方法。若是請求的 Host header 不驗證,不被信任的請求輸入就會在視圖或者page中的URIs被髮送到客戶端。咱們建議素有的生產用的應用程序都配置服務器驗證已知的值做爲 Host header 有效的值。
  • 在結合了 Map 或者 MapWhen 的中間件中謹慎使用 LinkGenerator。Map* 改變了執行請求的基本路徑,這會影響 link generator 的輸出。全部的 LinkGenerator APIs 容許指定一個基本的路徑。指定一個空的基本路徑能夠避免 Map* 對 link generation 產生的影響。

 中間件示例

在下面的例子中,一箇中間件使用了 LinkGenerator API 爲一個列出存儲產品的方法建立一個連接。經過注入 link generator 到一個類中,而後在任何一個應用程序中的類均可以調用 GenerateLink:

public class ProductsLinkMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var url = _linkGenerator.GetPathByAction("ListProducts", "Store");

        httpContext.Response.ContentType = "text/plain";

        await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
    }
}

路由模板參考

{} 中的符號定義了路由匹配時綁定的路由參數。能夠在路由分段中定義多個路由參數,可是路由參數必須由字面值分割開來。例如, {controller=Home}{action=Index} 不是一個有效的路由,因爲在 {controller} 和 {action} 之間沒有字面值。路由參數必須有一個名稱,也可能有更多指定的屬性。

字面值而不是路由參數(例如 {id}) 和路徑分隔符 / 必須匹配 URL 中的文本。文本匹配區分大小寫而且基於 URL's 路由的編碼。根據定界符 { 或者 } 匹配一個字面上的路由參數,經過重複字符轉義定界符。例如: {{ 或者 }}

星號 * 或者 兩個星號 **:

  • 能夠做爲路由參數的一個前綴,綁定到 URI 剩餘的部分
  • 被稱爲 catch-all 參數。例如,blog/{**slug}:匹配全部的以 /blog 開頭的以及跟有任何值得 URI;跟在 /blog 後面的值被賦值給 slug

Catch-all 參數也能夠匹配空的字符串。

當路由被用來生成一個 URL,catch-all 參數會轉義合適的字符,包括路徑分隔符 /。例如,帶有參數 { path = "my/path" } 的路由 foo/{*path} 生成 foo/my%2Fpath。注意轉義的斜槓。爲了保留路徑分隔符,使用 ** 做爲路徑參數的前綴。路由 foo/{**path} 賦值 { path="my/path" } 生成 foo/my/path。

視圖捕捉一個帶有可選的文件擴展名的文件名稱的 URL 模式的時候,須要有更多的考慮。例如,模板 files/{filename}.{ext?}。當參數 filename 和 ext 都有值得時候,兩個值都會被填充。若是隻有 filename 的值存在於 URL 中,路由將會匹配,由於尾部的 . 這是就是可選的。下面的 URLs 會匹配這兩種路由:

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

路由參數能夠提供默認值,經過在參數名稱後面添加等號(=)給路由參數指定。例如, {controller=Home},爲 controller 指定了 Home 做爲默認的值。默認的值在 URL 中沒有爲參數提供值的時候使用。經過在路由參數名稱後面添加一個問號 (?) 來指定這個參數是可選的。例如,id?。可選參數和默認參數不一樣的是:

  • 提供默認值得路由參數老是會生成一個值
  • 可選參數只有在請求的 URL 中提供值的時候纔會被賦予一個值

路由參數可能包含必須匹配綁定到 URL 中的路由值的約束。在路由參數名稱後面添加 : 和約束名稱在行內指定參數約束。若是約束要求帶有參數,它們被包括在圓括號(...)中跟在約束名稱後面。多個行內約束能夠經過添加另一個 : 和約束名稱來指定。

約束名稱和參數被傳遞給 IlnlineConstraintResolver 用來建立一個 IRouteConstratin 實例,這個實例在 URL 處理過程當中使用。例如,路由模板 blog/(article:minlength(10) 指定了一個值爲 10 的 minlength 約束。更多的關於路由約束和框架提供的一系列的約束,請查看 Route constraint reference 部分。

路由參數可能會有參數轉換。路由參數在生成連接和匹配方法以及pages到URLs的時候會轉換參數的值。和約束同樣,參數轉換能夠經過在路由參數名稱後面添加一個 : 和轉換名稱到路由參數。例如,路由模板 blog/{article:slugify} 指定一個名稱爲 slugify 的轉換。關於更過關於參數轉換的信息,請查看 Parameter transformer reference 部分。

下面的表格展現了路由模板和他們的行爲:

路由模板 匹配的 URI 示例 請求 URI...
hello /hello 只匹配一個路徑 /hello
{Page=Home} / 匹配而且設置 Page 爲 Home
{Page=Home} /Contact 匹配而且設置 Page 爲 Contact
{controller}/{action}/{id?} /Products/List 映射 Products 到 controller,List 到 action
{controller}/{action}/{id?} /Products/Details/123 映射 Products 到 controller,Details 到 action,id 的值被設置爲 123
{controller=Home}/{action=Index}/{id?} / 映射到 Home controller 和 Index 方法. id 參數被忽略
{controller=Home}/{action=Index}/{id?} /Products 映射到 Products controller 和 Index 方法,id 參數被忽略

 

一般的,使用模板是最簡單的獲取路由的方式。約束和默認值也能夠被指定在路由模板的外部。

複雜的分段

複雜的分段是經過以非貪婪的方式從右到左匹配文字分割的方式來處理的。例如,[Route("/a{b}c{d}")] 是一個複雜的分段路由。複雜的分段以一種特殊的方式工做,必須理解以可以正確的使用它們。這部分的示例展現了爲何複雜的分段只有在分界文本不在參數值中才能正常工做的緣由。使用正則表達式,而後對於更加複雜的例子須要手動提取其中的值。

⚠️  警告:

當使用 System.Text.RegularExpressions 處理不受信任的輸入的時候,傳入一個超時時間。一個惡意的用戶提供給 RegularExpressions 的輸入可能會引發 Denial-of-Service attack. ASP.NET Core 框架的 APIs 使用 RegularExpressions 的時候傳入了超時時間。

下面總結了路由處理模板 /a{b}c{d} 匹配 URL path /abcd 的步驟。| 用來使算法是怎麼工做的更加形象:

  • 從右到左計算,第一個文本值是 c。所以 /abcd 從右搜索而後發現 /ab|c|d
  • 右邊 (d)的全部如今匹配到路由參數 {d}
  • 從右到左計算,下一個是 a。所以 /ab|c|d 從咱們結束的地方開始搜索,而後 a 被發現 /a|b|c|d
  • 右邊的 (b) 如今匹配路由參數 {b}
  • 沒有了剩餘的文本和路由模板,所以一個匹配就完成了

這裏舉例一個使用相同模板 /a{b}c{d},不一樣 URL 路徑匹配不成功的例子。| 用來更形象的展現算法的工做。這個例子使用一樣的算法解釋了沒有匹配成功:

  • 從右到左,第一個文字是 c。所以從右開始搜索 /aabcd,發現了 /aab|c|d
  • 右邊的(d)的全部都匹配了路由參數 {d}
  • 從右到左,下一個文字是 a。所以從上次咱們中止的地方 /aab|c|d 開始搜索,a 在 /a|a|b|c|d 中被發現
  • 右邊的 (b) 如今匹配到路由參數 {b}
  • 此時,還有一個剩餘的字符 a,可是算法已經按照路由模板運行完畢,所以匹配沒有成功

因爲匹配算法是非貪婪的:

  • 在每個步驟中,它匹配最小數量的文本
  • 任何狀況下,在參數值內的分隔符的值都會致使不匹配

正則表達式提供了更多的匹配行爲。

貪婪匹配,也叫作懶匹配,匹配最大可能的字符串。非貪婪模式匹配最小的字符串。

 

路由約束參考

路由約束在匹配入站 URL 和 URL path 被路由值標記進入的時候會執行。路由約束經過路由模板檢測路由值,而後確認值是否能夠被接受。一些路由約束使用路由值以外的數據去考慮是否一個請求能夠被路由。例如,HttpMethodRouteConstraint 能夠基於它的 HTTP 謂詞接受或者拒絕一個請求。約束被用於路由請求和連接生成中。

⚠️  警告:

不要使用約束驗證輸入。若是約束被用於輸入驗證,無效的輸入將會致使 404 Not Found 被返回。無效的輸入應該產生一個帶有合適錯誤信息的 400 Bad Request。路由約束用來消除類似路由的歧義,而不是用來驗證一個特定的路由。

下面的表格展現了示例路由約束和它們指望的行爲:

約束 示例 匹配的示例 備註
int {id:int} 123456789,-123456789 匹配任何的整型數據
bool {active:bool} true,False 匹配 true,false。不區分大小寫
datetime {dob:datetime} 2020-01-02,2020-01-02 14:27pm 在固定區域中匹配一個有效的 DateTime 類型的值。查看處理警告。
decimal {price:decimal} 49.99,-1,000.01 在固定區域中匹配一個有效的 decimal 類型的值。查看處理警告。
double {weight:double} 1.234,-1,001.01e8 在固定區域中匹配一個有效的 double 類型的值。查看處理警告。
float {weight:float} 1.234,-1,001.01e8 在固定區域中匹配一個有效的 float 類型的值。查看處理警告。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 匹配一個有效的 guid 類型的值
long {ticks:long} 123456789,-123456789 匹配一個有效的 long 類型的值
minlength(value) {username:minlength(4)} Rick 長度至少爲 4 的字符串
maxlength(value) {filename:maxlength(8)} MyFile 長度最多爲 8 的字符串
length(length) {filename:length(12)} somefile.txt 長度爲 12 的字符串
length(min,max) {filename:length(8,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} Rick 字符串必須包含一個或者更多的字母字符,a-z,不區分大小寫
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 字符串必須匹配提供的正則表達式。查看定義正則表達式的提示。
required {name:required} Rick 在生成 URL 的過程當中用來強制一個非空參數值

⚠️  警告:

當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。

多個冒號分隔符能夠應用於單個的參數。例如,下面的約束限制一個最小爲1的整型:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) {}

⚠️  警告:

路由約束驗證 URL 而且老是使用固定的區域轉換爲一個 CLR 類型。例如,轉換爲 CLR 類型中的 int 或者 DateTime。這些約束假設 URL 不是本地化的。框架提供的路由約束不修改保存在路由值中的值。全部的路由值從 URL 中解析出來被保存爲字符串。例如,float 約束試圖轉換一個路由值爲 float 類型,可是轉換隻有在驗證能夠被轉換的時候纔會使用到。

約束中的正則表達式

⚠️  警告:

當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。

約束中的正則表達式可使用 regex(...) 在行內指定。 MapControllerRoute 一類的方法也接受對象字面值。若是使用了這種格式,字符串的值被解釋爲正則表達式。

下面的代碼使用了行內正則表達式約束:

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });

下面的代碼使用了字面對象指定一個正則表達式約束:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "people",
        pattern: "People/{ssn}",
        constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
        defaults: new { controller = "People", action = "List", });
});

ASP.NET Core 框架在正則表達式的構造方法中添加了 RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant 參數。查看 RegexOptions 瞭解這些成員的描述。

正則表達式使用分隔符和符號和路由以及 C# 語言類似。正則表達式的符號必須被轉義。在約束行內使用正則表達式 ^\d{3}-\d{2}-\d{4}$,可使用如下任意一種方法:

爲了轉義路由參數分隔符 {,},[,],在表達式中使用重複的字符,例如,{{,}},[[,]]。下面的表格展現了正則表達式和它的轉義版本:

正則表達式 轉義後的正則表達式
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

路由中的正則表達式常常是 ^ 字符開頭去匹配字符串的起始位置。正則表達式老是以 $ 結尾來匹配字符串的結尾。 ^ 和 $ 字符保證了正則表達式可以匹配所有的路由參數值。若是沒有 ^ 和 $,正則表達式匹配任意的子字符串,這不是咱們指望獲得的。下面的表格列出了一些例子,而且解釋了爲何可以匹配或者匹配失敗:

表達式 字符串 是否匹配 備註
[a-z]{2} hello YES 子字符串匹配
[a-z]{2} 123abc456 YES 子字符串匹配
[a-z]{2} mz YES 匹配正則表達式
[a-z]{2} MZ YES 不區分大小寫
^[a-z]{2}$ hello NO 查看上面 ^ 和 $
^[a-z]{2}$ 123abc456 NO 查看上面 ^ 和 $

更多關於正則表達式語法的信息,查看 .NET Framework Regular Expressions.

使用正則表達式能夠約束參數到一些已知的可能的值上面。例如,{action:regex(^(list|get|create)$)} 僅僅匹配 action 的路由值到 list,get 或者 create。若是傳遞到約束字典中,字符串 ^(list|get|create)$) 是等同的。傳入約束字典的約束若是不匹配任意一個一直的約束,那麼任然被認爲是一個正則表達式。使用模板傳入的約束若是不匹配任意一個已知的約束將不被認爲是正則表達式。

 

自定義路由約束

經過實現 IRouteConstraint 接口能夠建立自定義的路由約束。接口 IRouteConstraint 包含 Match,當知足約束的時候它會返回 true,不然返回 false。自定義約束不多被用到。在實現一個自定義約束以前,考慮更直接的方法,好比模型綁定。

ASP.ENT Core 約束文件夾提供了一個很好的建立約束的例子。例如,GuidRouteConstraint。

爲了使用一個自定義的 IRouteConstraint,路由約束的類型必須使用 ConstraintMap 在服務容器中註冊。一個 CostraintMap 是一個映射路由約束鍵值和 驗證這些約束 IRouteConstraint 實現的字典。應用程序的 ConstraintMap 能夠在 Startup.ConfigureServices中或者做爲 services.AddRouting 調用的一部分或者直接經過 services.Configure<RouteOptions>配置 RouteOptions 來更新。例如:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}

上面添加的約束在下面的代碼中使用:

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    // GET /api/test/3
    [HttpGet("{id:customName}")]
    public IActionResult Get(string id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // GET /api/test/my/3
    [HttpGet("my/{id:customName}")]
    public IActionResult Get(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

MyDisplayRouteInfo 經過 Rick.Docs.Samples.RouteInf NuGet 包提供,用來顯示路由信息。

MyCustomConstraint 約束的實現阻止 0 被賦值給路由參數:

class MyCustomConstraint : IRouteConstraint
{
    private Regex _regex;

    public MyCustomConstraint()
    {
        _regex = new Regex(@"^[1-9]*$",
                            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
                            TimeSpan.FromMilliseconds(100));
    }
    public bool Match(HttpContext httpContext, IRouter route, string routeKey,
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out object value))
        {
            var parameterValueString = Convert.ToString(value,
                                                        CultureInfo.InvariantCulture);
            if (parameterValueString == null)
            {
                return false;
            }

            return _regex.IsMatch(parameterValueString);
        }

        return false;
    }
}

⚠️  警告:

當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。

前面的代碼:

  • 禁止0賦值給 {id}
  • 展現了實現一個自定義約束的基本示例。它不該該被用於一個正式環境的應用程序中。

下面的代碼展現了一個更好的禁止0賦值給 id 的處理過程:

[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return ControllerContext.MyDisplayRouteInfo(id);
}

上面的代碼比自定義的約束 MyCustomConstraint 有如下優點:

  • 不用實現一個自定義的約束
  • 當路由參數包含 0 的時候,會返回一個更加描述性的錯誤信息

參數轉換參考

參數轉換:

例如,一個在模型 blog\{article:slugify} 中的自定義的 slugfy 參數轉換時使用 Url.Action(new { artical = "MyTestArtical" }) 生成 blog\my-test-artical。

考慮下面 IOutboundParameterTransformer 的實現:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(), 
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}

在參數模型中爲了使用一個參數轉換,須要在 Startup.ConfigureServices 中使用 ConstraintMap 配置。以下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

ASP.NET Core 框架在一個 enpoint 被解析到的時候會使用參數轉換去轉換 URI。例如,參數轉換會轉換被用來匹配一個 area, controller,action,和page 的路由值。

例如如下代碼:

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

對於上面這個路由模板,方法 SubscriptionManagementController.GetAll 匹配了 URI /subscription-management/get-all。參數轉換並無更改用來生成一個連接的路由值。例如,Url.Action("GetAll", "SubscriptionManagement") 輸出 /subscription-management/get-all.

ASP.NET Core 提供了供生成的路由使用的參數轉換 API 的約定:

URL 生成參考

這部分包含了 URL 生成算法的參考。在實際中,大多複雜的 URL 生成使用控制器或者 Razor Pages。查看 routing in controllers 獲取更多信息。

URL 生成過程一開始調用 LinkGenerator.GetPathByAddress 或者一個類似的方法。這個方法提供一個地址,一組路由值和關於從 HttpContext 獲取到的當前請求的可選的信息。

第一步就是使用地址去解析一組候選的 endpoints,這些 endpoints 使用 IEndpointAddressScheme<TAddress> 去匹配地址的類型。

一旦根據地址架構獲取到了一組候選 endpoints,endpoints 將會被排序,而後迭代處理直到一個 URL 生成的操做成功。URL 生成不檢查歧義性,第一個返回的結果就是最終的結果。

使用日誌跟蹤 URL 生成

跟蹤 URL 生成的第一步就是設置日誌等級由 Microsoft.AspNetCore.Routing 到 TRACE。LinkGenerator 記錄了不少關於對解決問題有用的處理過程的詳細信息。

查看 URL generation reference 關於 URL 生成的詳細信息。

地址

地址的概念是 URL 生成用來綁定一個連接生成器中的一個調用到一組候選的 enpoints。

地址是隨兩個默認實現擴展出來的概念:

  • 使用 enpoint 名稱 (string) 做爲地址:
    爲 MVC的路由名稱提供類似的功能
    使用 IEndpointNameMetadata 做爲 metadata 的類型
    根據全部註冊的 enpoints 的 metadata 解析提供的字符串
    若是多個 endpoints 使用相同的名稱,將會在 startup 中拋出異常
    對於通用目的的使用,推薦在控制器和 Razor Pages 以外使用
  • 使用路由值 (RouteValuesAddress)做爲地址
    提供相似於控制器和 Razor Pages 遺留的 URL 生成的功能
    擴展和調試很是複雜
    提供 IUrlHelper,Tag Helpers,HTML Helpers,Action Result 等等使用的實現

地址架構的做用是經過任意條件在地址和 enpoints 匹配之間創建關聯。

環境值和顯式值

從當前請求中,路由從 HttpContext.Request.RouteValues 中獲取路由值。和當前請求關聯的值被稱爲環境值。爲了更清晰,文檔中把路由值中傳遞給方法的值稱爲顯式值。

下面的例子展現了環境值和顯式值。它提供了從當前請求中獲取的環境值和顯式值: { id = 17 }

public class WidgetController : Controller
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public IActionResult Index()
    {
        var url = _linkGenerator.GetPathByAction(HttpContext,
                                                 null, null,
                                                 new { id = 17, });
        return Content(url);
    }

上面代碼:

  • 返回了 /Widget/Index/17
  • 經過 DI (依賴注入)獲取 (LinkGenerator

下面的例子展現了不提供環境值,提供顯式值: { controller = "Home", action = "Subscribe", Id = 17 }:

public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}

上面的方法返回 /Home/Subscribe/17

WidgetController 中下面的代碼返回 /Widget/Subscribe/17:

var url = _linkGenerator.GetPathByAction("Subscribe", null,
                                         new { id = 17, });

下面的代碼提供了從當前請求的環境值中獲取的控制器,顯式值: { action = "Edit", id = 17 }:

public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }

在上面的代碼中:

  • /Gadget/Edit/17 被返回
  • Url 獲取 IUrlHelper.
  • Action 爲 action 方法生成了一個絕對路徑的 URL

下面的代碼提供了從當前請求中獲取的環境值,以及顯式值: { page = ".Edit", id = 17 }:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var url = Url.Page("./Edit", new { id = 17, });
        ViewData["URL"] = url;
    }
}

上面的代碼在當 Edit Razor Page 包含如下指令的時候會設置 url 爲 /Edit/17:

@page "{id:int}"

若是 Edit page 不包含 "{id:int}" 路由模板,url 就是 /Edit?id=17.

MVC 的 IUrlHelper 又增長了一層複雜性,除了下面描述的規則外:

  • IUrlHelper 老是從當前請求中獲取路由值做爲環境值
  • IUrlHelper.Action 老是複製當前的 action 和 controller 的路由值做爲顯式值,除非他們被重寫
  • IUrlHelper.Page 老是複製當前 page 的路由值做爲顯式值,除非 page 被重寫
  • IUrlHelper.Page 老是覆蓋當前 handler 的路由值爲 null 來做爲顯式值,除非被重寫

用戶老是對環境值的詳細行爲感到驚訝,由於 MVC 彷佛不跟隨它本身的規則。因爲歷史和兼容性的緣由,肯定的路由值,例如 action,controller,page 和 handler 都有它們本身特定的行爲。

LinkGenerator.GetPathByAction 和 LinkGenerator.GetPathByPage 提供的相同功能爲了兼容性複製了 IUrlHelper 的這些異常。

URL 生成處理

一旦一組候選的 enpoints 被發現了,接下來就是 URL 生成算法:

  • 迭代的處理 enpoints
  • 返回第一個成功的結果

處理過程的第一不叫作路由值驗證。路由值驗證經過路由決定從環境值獲取到的路由值哪一個應該被使用和哪一個應該被忽略。每個環境值都會被考慮是結合顯示值仍是被忽略。

理解環境值得最好方式是在一般狀況下認爲它試圖節省應用程序開發者的輸入。傳統的,環境值使用的場景對相關的 MVC 是很是有用的:

  • 當連接到同一個控制器的其餘方法時,控制器的名稱沒必要指定
  • 當連接到同一 area 中的其餘控制器時,area 的名稱沒必要指定
  • 當連接到同一個方法時,路由值沒必要指定
  • 當連接到應用程序的另一部分時,你不想攜帶一些對其餘部分沒有意義的路由值

調用 LinkGenerator 或者 IUrlHelper 返回 null 的狀況一般是因爲沒有經過路由值驗證。調試路由值驗證,能夠經過顯式指定更多的路由值來查看問題是否解決。

路由值無效的前提是假設應用程序 URL 架構是分層的,擁有一個從左到右的分層結構。考慮一個基本的路由模板 {controller}/{action}/{id?} 能夠直觀的感覺在實際中它是怎麼工做的。對一個值的更改會使得出如今右邊的全部路由值失效。這反映了關於層次結構的假設。若是應用程序中 id 有一個環境值,而且操做給控制器指定了一個不一樣的值:

  • id 不會被重複使用,由於 {controller} 在 {id} 的左邊

一些示例展現了這個原則:

  • 若是顯式值中包含了 id 的值,環境中的 id 的值就會被忽略。環境值中的 controller 和 action 能夠被使用
  • 若是顯式值中包含了 action 的值,任何環境值中 action 的值都會被忽略。環境值中的 controller 會被使用。若是顯式值中的 action 的值和環境值中的 action 中的值不一樣,id 的值將不會被使用。若是 action 的值相同,則 id 的值會被使用。
  • 若是顯式值中包含了 controller 的值,任何環境中的 controller 的值都被忽略。若是顯式值中的 controller 和環境值得 controller 值不相同,action 和 id 的值不會被使用。若是 controller 中的值相同,action 和 id 的值會被使用。

對於現存的屬性路由和專用常規路由,這一處理過程更加複雜。控制器常規路由,例如 {controller}/{action}/{id?} 使用路由參數指定了一個分層結構。 控制器和 Razor Pages 中的常規路由和屬性路由:

  • 有一個分層的路由值
  • 它們不出如今路由模板中

對於這些狀況, URL 生成定義了 required values 的概念。controllers 和 Razor Pages 建立的 endpoints 能夠指定容許路由值驗證工做的 required values。

路由值驗證算法的詳細信息:

  • required value 名稱和路由參數結合,而後從左到右處理
  • 對於每個參數,環境值和顯式值都會被比較:
    若是環境值和顯式值相同,處理過程繼續
    若是有環境值沒有顯式值,環境值被用來生成 URL
    若是有顯式值沒有環境值,則拒絕環境值和後續的全部環境值
    若是環境值和顯示值都存在,而且兩個值不相同,則拒絕環境值和後續的全部環境值

這是,URL 生成操做已經準備好開始評估路由約束。接受的值的集合與提供給約束的默認的參數值相結合。若是約束所有經過,操做將會繼續。

下一步,被接受的參數能夠用來展開路由模板。路由模板的處理過程以下:

  • 從左到右
  • 每個參數都會替代被接受的值
  • 有如下特殊狀況
    若是沒有被接受的值,可是參數有一個默認的值,默認的值會被使用
    若是沒有被接受的值,而且參數是可選的,則過程繼續
    若是缺失的可選參數的右邊的路由參數有任何值,則操做失敗
    連續的默認參數值和可選參數可能會被摺疊

不匹配路由分段的顯式的值被添加到 query 字符串中。下面的表格展現了使用路由模板 {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

路由值驗證的問題

ASP.NET Core 3.0 中一些 URL 生成的架構在早期的 ASP.ENT Core 的版本中 URL 生成工做的並很差。ASP.NET Core 團隊計劃在將來的發佈版本中添加新的特性的需求。目前,最好的解決方法就是使用傳統路由。

下面的代碼展現了 URL 生成架構不被路由支持的示例:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});

在上面的代碼中,culture 路由參數被用來本地化。指望的是 culture 參數老是被接受爲一個環境值。然而,culture 參數不被接受爲一個環境值,由於 required values 的工做方式。

  • 在 "default" 路由模板中,culture 路由參數在 controller 的左邊,所以對於 controller 的更改不會驗證 culture
  • 在 "blog" 路由模板中,culture 路由參數被認爲是在 controller 的右邊,出如今了 required values 裏面

配置 endpoint metadata

下面的連接提供了配置 enpoint metadata 的更多信息:

路由中主機匹配與 RequireHost

RequireHost 應用一個約束到須要指定主機的路由。RequireHost 或者 [Host] 參數能夠是:

  • Host: www.domain.com,匹配任意端口的 www.domain.com
  • 帶有通配符的 Host: *.domain.com,匹配任意端口自的 www.domain.com,subdomain.domain.com 或者 www.subdomain.domain.com
  • 端口:*:5000,匹配任意5000端口的 Host
  • Host 和 port: www.domain.com:5000 或者 *.domain.com:5000,匹配 host 和 port

使用 RequireHost 或者 [Host] 能夠指定多個參數。匹配主機的約束驗證任意的參數。例如,[Host("domain.com","*domain.com")] 匹配 domain.com,www.domain.com 和 subdomain.domain.com。

下面的代碼使用 RequireHost 要求在路由中指定主機:

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}

下面的代碼在 controller 上使用 [Host] 屬性要求任意指定的主機:

[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

當 [Host] 屬性在 controller 和 action 方法上都應用了的狀況:

  • action 上面的屬性被使用
  • controller 屬性被忽略

路由性能指南

大部分的路由在 ASP.NET Core 3.0 被更新提升了性能。

當一個應用程序出現性能問題的時候,路由老是被懷疑是問題所在。路由被懷疑的緣由是像 controllers 和 Razor Pages 這樣的框架在它們的日誌信息中報告了在框架內部花費了大量的時間。當 controllers 報告的時間和請求的總的時間有很大不一樣的時候:

  • 開發者消除了他們應用程序代碼是問題的根源
  • 一般會懷疑是路由引發的

路由使用了成千上萬的 enpoints 來測試性能。一個典型的應用程序不太可能僅僅由於太大而遇到應能問題。路由性能緩慢最多見的根本緣由是因爲很差的自定義的中間件引發的。

下面的代碼展現了縮小延遲來源的基本技術:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

對於時間路由:

  • 在上面的代碼中,時間中間件交錯在每個中間件中間
  • 在代碼中添加一個惟一的標識關聯時間數據

這是一種最基本的縮小延遲的方法,當延遲很嚴重的時候,例如超過 10ms。Time 2 減去 Time1 就是 UseRouting 中間件花費的時間。

相比前面的代碼,下面的代碼使用了一個更加緊湊的方法:

public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;

    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }

    private bool disposed = false;


    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
                                    _message, _sw.ElapsedMilliseconds);

            disposed = true;
        }
    }
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }

    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}

潛在的昂貴的路由特性

下面列表展現了相比基本路由模板開銷更多的路由特性分析:

  • 正則表達式:可能會編寫複雜的正則表達式,或者較少的輸入就會致使長時間的運行
  • 複雜分段({x}-{y}-{z}):
    要比解析一個正則表達式 URL 路徑分段更加複雜
    致使更多的子字符串的開銷
    ASP.NET Core 3.0 中複雜分段邏輯並無更新,路由的性能也沒有更新
  • 異步數據獲取:許多複雜的應用程序在它們的路由中都會訪問數據庫。ASP.NET Core 2.2 以及以前的版本路由沒有提供路由訪問數據庫的功能。例如,IRouteConstraint,IActionConstraint 是同步的。擴展的 MatcherPolicy 和 EndpointSelectorContext 是異步的。

庫做者指南

這部分包含了創建在路由之上的庫編寫者指南。這些細節目的是爲了保證應用程序的開發者在使用庫和框架擴展路由的時候能有一個好的體驗。

定義 endpoints

建立一個使用路由實現 URL 匹配的框架,開始須要定義一個創建在 UesEnpoints 之上的用戶體驗。

保證 在 IEndpointRouteBuilder 之上開始創建。這運行用戶把你的框架和其它 ASP.NET Core 特性很好的構造在一塊兒。每個 ASP.NET Core 模板都包含路由。假設路由已經存在而且對用戶來講很熟悉。

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});

保證 調用 MapMyFramework(...) 返回一個具體的實現了 IEndpointConventionBuilder 的類型。大多數的框架 Map... 方法遵循這個模型。IEndpointConventionBuilder 接口;

  • 容許組合 metadata
  • 以各類擴展方法爲目標

聲明你本身的類型容許你添加你本身框架特有的功能到 builder 中。封裝一個框架聲明的 builder 而後去調用它是可行的。

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});

考慮編寫你本身的 EndpointDataSource. EndpointDataSource 用來聲明和更新 endpoints 集合的低級原語。EndpointDataSource 是一個功能強大的 API,被 controllers 和 Razor Pages 使用。

路由測試包含一個不更新 data source 的基本示例。

不要試圖默認註冊一個 EndpointDataSource。要求用戶在 UseEndpoint 中註冊你的框架。路由的哲學就是默認什麼都不包含, UseEndpoints 就是註冊 endpoints 的地方。

建立一個路由集成的中間件

考慮定義 metadata 類型做爲一個接口

保證 可以在類和方法上面使用 metadata 類型。

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

像 controllers 和 Razor Pages 這樣的框架支持應用 metadata 屬性到類型和方法。若是你聲明瞭 metadata 類型:

  • 使得它們能夠做爲 attributes 被獲取
  • 大多數用戶熟悉應用屬性

聲明 metadata 類型爲一個接口增長了另一層靈活性:

  • 接口是可組合的
  • 開發者能夠聲明結合多個策略的類型

保證 metadata 可以被重寫,就像下面展現的例子同樣:

public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}

遵照這些指南的最好的方式是避免定義 maker metadata:

  • 不要只尋找 metadata 類型的存在
  • 定義一個 metadata 的屬性而且要檢查這個屬性

metadata 集合是根據優先級排序和支持重寫的。在控制器的狀況下,action 上的 metadata 是最肯定的。

保證 不論有沒有路由中間件都應該有用。

app.UseRouting();

app.UseAuthorization(new AuthorizationPolicy() { ... });

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});

做爲這個指南的一個例子,考慮使用 UseAuthorization 中間件。authorization 中間件容許你傳遞一個反饋策略。若是反饋策略被指定了,將會應用到:

  • 沒有指定策略的 endpoints
  • 不須要匹配 endpoint 的請求

這使得 authorization 中間件在路由上下文以外也有做用。authorization 中間件能夠被用作傳統中間件編程。

調試診斷

對於更詳細的路由調試輸出,設置 Logging:LogLevel:Microsoft 爲 Debug。在開發環境中,在 appsettings.Development.json 中設置日誌等級:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
相關文章
相關標籤/搜索