ASP.NET Core 中的路由

作者:Ryan NowakKirk LarkinRick Anderson

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

路由負責比對連入 HTTP 要求,並將這些要求分派至應用程式的可執行端點。 端點是應用程式的可執行檔要求處理程式碼單位。 端點會在應用程式中定義,並在應用程式啟動時設定。 端點比對程式可以從要求的 URL 擷取值,並提供這些值以供要求處理。 使用來自應用程式的端點資訊,路由也能夠產生對應至端點的 URL。

應用程式可以使用下列方式設定路由:

本文涵蓋路由 ASP.NET Core 低層級詳細資料。 如需設定路由的資訊:

路由的基本概念

下列程式碼顯示路由的基本範例:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上述範例包含使用 MapGet 方法的單一端點:

  • 當 HTTP GET 要求傳送至根 URL / 時:
    • 要求委派會執行。
    • Hello World! 會寫入 HTTP 回應。
  • 如果要求方法不是 GET 或根 URL 不是 /,則不會比對路由,而且會傳回 HTTP 404。

路由使用一對中介軟體,由 UseRoutingUseEndpoints 註冊:

  • UseRouting 會將路由比對新增至中介軟體管線。 此中介軟體會查看應用程式中定義的端點集,並根據要求選取最佳端點比對
  • UseEndpoints 將端點執行新增至中介軟體管線。 它會執行與所選端點相關聯的委派。

應用程式通常不需要呼叫 UseRoutingUseEndpointsWebApplicationBuilder 設定中介軟體管線,此管線會包裝使用 UseRoutingUseEndpointsProgram.cs 中新增的中介軟體。 不過,應用程式可以藉由明確呼叫這些方法,變更 UseRoutingUseEndpoints 執行的順序。 例如,下列程式碼會明確呼叫 UseRouting

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

在上述程式碼中:

  • app.Use 的呼叫會註冊在管線開頭執行的自訂中介軟體。
  • 呼叫 UseRouting 會設定路由比對中介軟體,以在自訂中介軟體之後執行。
  • MapGet 註冊的端點會在管線結尾執行。

如果上述範例未包含對 UseRouting 的呼叫,則自訂中介軟體會在路由比對中介軟體之後執行。

注意: 路由直接新增至管線尾端WebApplication 執行。

端點

MapGet 方法可用來定義端點。 端點可以是:

  • 選取,方法是比對 URL 和 HTTP 方法。
  • 執行,透過執行委派。

應用程式可以比對和執行的端點會在 UseEndpoints 中設定。 例如,MapGetMapPost類似的方法,將要求委派連接到路由系統。 其他方法可用來將 ASP.NET Core 架構功能連線至路由系統:

下列範例示範使用更複雜的路由範本進行路由:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

字串 /hello/{name:alpha}路由範本。 路由範本可用來設定端點的比對方式。 在此情況下,範本會比對:

  • URL,例如 /hello/Docs
  • 開頭為 /hello/ 後面接著一連串字母字元的任何 URL 路徑。 :alpha 套用只符合字母字元的路由條件約束。 本文稍後會說明路由條件約束

URL 路徑的第二個區段 {name:alpha}

下列範例顯示具有健康狀態檢查和授權的路由:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上述範例示範如何:

  • 授權中介軟體可以搭配路由使用。
  • 端點可用來設定授權行為。

MapHealthChecks 呼叫會新增健康情況檢查端點。 鏈結 RequireAuthorization 至此呼叫會將授權原則附加至端點。

呼叫 UseAuthenticationUseAuthorization 以新增驗證和授權中介軟體。 這些中介軟體會放在 UseRoutingUseEndpoints 之間,讓它們能夠:

  • 查看 UseRouting 所選取的端點。
  • UseEndpoints 分派至端點之前,請先套用授權原則。

端點中繼資料

在上述範例中,有兩個端點,但只有健康情況檢查端點已附加授權原則。 如果要求符合健康情況檢查端點 /healthz,則會執行授權檢查。 這示範端點可以附加額外的資料。 此額外資料稱為端點中繼資料

  • 中繼資料可以透過路由感知中介軟體來處理。
  • 中繼資料可以是任何 .NET 類型。

路由概念

路由系統會藉由新增功能強大的端點概念,以建置在中介軟體管線之上。 端點代表應用程式功能單位,這些單位在路由、授權和任何數目 ASP.NET Core 的系統方面彼此不同。

ASP.NET Core 端點定義

ASP.NET Core 端點為:

下列程式碼示範如何擷取和檢查符合目前要求的端點:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

如果選取端點,可以從 HttpContext 擷取。 可以檢查其屬性。 端點物件是不可變的,在建立之後無法修改。 最常見的端點類型是 RouteEndpointRouteEndpoint 包含允許路由系統選取的資訊。

在上述程式碼中,app.Use 設定內嵌中介軟體

下列程式碼顯示,視管線中呼叫 app.Use 的位置而定,可能沒有端點:

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

app.UseRouting();

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

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

app.UseEndpoints(_ => { });

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

上述範例會新增 Console.WriteLine 語句,以顯示是否已選取端點。 為了清楚起見,此範例會將顯示名稱指派給提供的 / 端點。

上述範例也包含呼叫 UseRoutingUseEndpoints,以完全控制這些中介軟體在管線內執行時間。

執行具有 / URL 的程式碼會顯示:

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

使用任何其他 URL 執行此程式碼會顯示:

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

此輸出示範:

  • 呼叫 UseRouting 之前,端點一律為 null。
  • 如果找到相符專案,則端點在 UseRoutingUseEndpoints 之間為非 null。
  • 找到相符項目時,UseEndpoints 中介軟體是終端。 本文稍後會定義終端中介軟體
  • 只有在找不到相符項目時,才會執行 UseEndpoints 後的中介軟體。

UseRouting 中介軟體會使用 SetEndpoint 方法來將端點附加至目前的內容。 您可以使用自訂邏輯取代 UseRouting 中介軟體,但仍可取得使用端點的權益。 端點是低層級基本類型,例如中介軟體,不會與路由實作結合。 大部分的應用程式不需要以自訂邏輯取代 UseRouting

UseEndpoints 中介軟體的設計目的是與 UseRouting 中介軟體搭配使用。 執行端點的核心邏輯並不複雜。 使用 GetEndpoint 來擷取端點,然後叫用其 RequestDelegate 屬性。

下列程式碼示範中介軟體如何影響或回應路由:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

上述範例示範兩個重要概念:

  • 中介軟體可以在 UseRouting 之前執行,以修改路由運作的資料。
  • 中介軟體可以在 UseRoutingUseEndpoints 之間執行,以在執行端點之前處理路由的結果。
    • UseRoutingUseEndpoints之間執行的中介軟體:
      • 通常會檢查中繼資料以了解端點。
      • 通常會依照 UseAuthorizationUseCors 來完成安全性決策。
    • 中介軟體和中繼資料的組合可讓您設定每個端點的原則。

上述程式碼示範支援每個端點原則的自訂中介軟體範例。 中介軟體會將敏感性資料存取權的稽核記錄寫入主控台。 中介軟體可以設定為使用 RequiresAuditAttribute 中繼資料端點稽核。 此範例示範加入宣告模式,其中只會稽核標示為敏感性的端點。 您可以反向定義此邏輯,例如稽核未標示為安全的所有內容。 端點中繼資料系統具有彈性。 此邏輯可以設計成任何適合使用案例的方式。

上述範例程式碼旨在示範端點的基本概念。 此範例不適用於生產環境稽核記錄中介軟體的更完整版本會:

  • 登入檔案或資料庫。
  • 包含詳細資料,例如使用者、IP 位址、敏感性端點的名稱等。

稽核原則中繼資料 RequiresAuditAttribute 定義為 Attribute,以便更輕鬆地與控制器和 SignalR 等類別架構搭配使用。 使用 路由至程式碼 時:

  • 中繼資料會與產生器 API 連結。
  • 以類別為基礎的架構會在建立端點時,包含對應方法和類別上的所有屬性。

中繼資料類型的最佳做法是將它們定義為介面或屬性。 介面和屬性允許重複使用程式碼。 中繼資料系統具有彈性,而且不會施加任何限制。

比較終端中介軟體與路由

下列範例示範終端中介軟體和路由:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: 顯示的中介軟體樣式為終端中介軟體。 它稱為終端中介軟體,因為它會執行比對作業:

  • 上述範例中的比對作業針對中介軟體是 Path == "/",針對路由是 Path == "/Routing"
  • 比對成功時,它會執行一些功能並傳回,而不是叫用 next 中介軟體。

它稱為終端中介軟體,因為它會終止搜尋、執行某些功能,然後傳回。

下列清單會比較終端中介軟體與路由:

  • 這兩種方法都允許終止處理管線:
    • 中介軟體會傳回而不是叫用 next 來終止管線。
    • 端點一律為終端。
  • 終端中介軟體允許將中介軟體放置在管線中的任意位置:
  • 終端中介軟體可讓任意程式碼判斷中介軟體何時符合:
    • 自訂路由比對程式碼可能很詳細且難以正確撰寫。
    • 路由為一般應用程式提供直接的解決方案。 大部分的應用程式都不需要自訂路由比對程式碼。
  • 端點介面與中介軟體,例如 UseAuthorizationUseCors
    • 使用具有 UseAuthorizationUseCors 的終端中介軟體需要手動與授權系統互動。

端點定義兩者:

  • 處理要求的委派。
  • 任意中繼資料的集合。 中繼資料是用來根據附加至每個端點的原則和組態來實作跨領域考量。

終端中介軟體可以是有效的工具,但可能需要:

  • 大量的程式碼撰寫和測試。
  • 手動與其他系統整合,以達到所需的彈性層級。

在撰寫終端中介軟體之前,請考慮與路由整合。

MapMapWhen 整合的現有終端中介軟體通常可以轉換成路由感知端點。 MapHealthChecks 示範路由器軟體的模式:

下列程式碼示範如何使用 MapHealthChecks

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

上述範例顯示為何傳回建立器物件很重要。 傳回建立器物件可讓應用程式開發人員設定原則,例如端點的授權。 在此範例中,健康情況檢查中介軟體與授權系統沒有直接整合。

中繼資料系統是針對使用終端中介軟體的擴充性建立者所遇到的問題而建立的。 每個中介軟體實作自己的與授權系統的整合是有問題的。

URL 比對

  • 這是路由比對傳入要求與端點的流程。
  • 以 URL 路徑和標頭中的資料為基礎。
  • 可以擴充以考慮要求中的任何資料。

當路由中介軟體執行時,它會設定 Endpoint,並將值路由至目前要求 HttpContext 上的要求功能

在路由中介軟體之後執行的中介軟體可以檢查端點並採取動作。 例如,授權中介軟體可以詢問授權原則的端點中繼資料集合。 執行要求處理管線中的所有中介軟體之後,會叫用所選端點的委派。

端點路由中的路由系統負責制定所有分派決策。 因為中介軟體會根據選取的端點套用原則,因此請務必:

  • 任何可能影響分派或安全性原則應用的決定,都會在路由系統內進行。

警告

為了回溯相容性,當執行 Controller 或 Razor頁面端點委派時,RouteContext.RouteData 的屬性會根據到目前為止所執行的要求處理,設定為適當的值。

RouteContext 類型將在未來的版本中標示為過時:

  • RouteData.Values 移轉至 HttpRequest.RouteValues
  • 移轉 RouteData.DataTokens 以從端點中繼資料擷取 IDataTokensMetadata

URL 比對會在一組可設定的階段中運作。 在每個階段中,輸出是一組相符項目。 下一個階段可以進一步縮小一組相符項目。 路由實作不保證相符端點的處理順序。 所有可能的相符項目都會一次處理。 URL 比對階段會依下列順序進行。 ASP.NET Core:

  1. 針對一組端點及其路由範本處理 URL 路徑,並收集所有相符項目。
  2. 採用上述清單,並移除套用路由條件約束失敗的相符項目。
  3. 採用上述清單,並移除失敗 MatcherPolicy 執行個體集的相符項目。
  4. 使用 EndpointSelector 從上述清單做出最終決定。

端點清單會根據下列項目排定優先順序:

每個階段都會處理所有相符的端點,直到到達 EndpointSelector 為止。 EndpointSelector 是最後階段。 它會從相符項目中選擇最高的優先順序端點作為最佳相符項目。 如果其他相符項目具有與最佳相符項目相同的優先順序,則會擲回模棱兩可的比對例外狀況。

路由優先順序是根據更具體路由範本來計算,其優先順序較高。 例如,請考慮範本 /hello/{message}

  • 兩者都符合 URL 路徑 /hello
  • /hello 更具體,因此優先順序較高。

一般而言,路由優先順序會針對實際使用的 URL 配置類型,選擇最相符的作業。 只有在需要時才使用 Order 以避免模棱兩可。

由於路由所提供的擴充性種類,路由系統無法事先計算模棱兩可的路由。 請考慮路由範本 /{message:alpha}/{message:int} 等範例:

  • alpha 條件約束只符合字母字元。
  • int 條件約束只符合數字。
  • 這些範本具有相同的路由優先順序,但兩者都沒有相符的單一 URL。
  • 如果路由系統在啟動時回報模棱兩可的錯誤,則會封鎖此有效的使用案例。

警告

UseEndpoints 內的作業順序不會影響路由的行為,但有一個例外狀況。 MapControllerRouteMapAreaRoute 會根據叫用的順序,自動將訂單值指派給其端點。 這會模擬控制器的長期行為,而沒有路由系統提供與舊版路由實作相同的保證。

ASP.NET Core 中的端點路由:

  • 沒有路由的概念。
  • 不提供訂購保證。 所有端點都會一次處理。

路由範本優先順序和端點選取順序

路由範本優先順序是一個系統,會根據路由範本的特定程度,為每個路由範本指派值。 路由範本優先順序:

  • 避免在常見情況下調整端點的順序。
  • 嘗試比對路由行為的常識預期。

例如,請考慮範本 /Products/List/Products/{id}。 對於 URL 路徑 /Products/List,可以合理地假設 /Products/List/Products/{id} 更相符。 這可運作,因為常值區段 /List 的優先順序會比參數區段 /{id} 更好。

優先順序運作方式的詳細資料會與如何定義路由範本:

  • 具有更多區段的範本會被視為更具體。
  • 具有常值文字的區段會比參數區段更具體。
  • 具有條件約束的參數區段會被視為比不具條件的參數區段更具體。
  • 複雜區段會被視為具有條件約束的參數區段。
  • Catch-all 參數是最不具體的。 如需 catch-all 路由的重要資訊,請參閱路由範本一節中的 catch-all

URL 產生概念

URL 產生:

  • 這是路由可以根據一組路由值建立 URL 路徑的流程。
  • 允許在端點與存取端點的 URL 之間進行邏輯分隔。

端點路由包含 LinkGenerator API。 LinkGenerator 是單一資料庫,可從 DI 取得。 LinkGenerator API 可以在執行要求的內容之外使用。 Mvc.IUrlHelper 和依賴 IUrlHelper 的情節,例如標籤協助程式、HTML 協助程式,以及動作結果,請在內部使用 LinkGenerator API 來提供連結產生功能。

連結產生器背後支援的概念為「位址」和「位址配置」。 位址配置可讓您判斷應考慮用於連結產生的端點。 例如,路由名稱和路由值情節,許多使用者都熟悉來自控制器,Razor 頁面會實作為位址配置。

連結產生器可以透過下列擴充方法連結至控制器和 Razor 頁面:

這些方法的多載會接受包含 HttpContext 的引數。 這些方法的功能相當於 Url.ActionUrl.Page,但提供額外的彈性和選項。

GetPath* 方法最類似 Url.ActionUrl.Page,因為它們會產生包含絕對路徑的 URI。 GetUri* 方法一律會產生包含配置和主機的絕對 URI。 接受 HttpContext 的方法會在執行要求的內容中產生 URI。 除非覆寫,否則會使用來自執行要求的環境路由值、URL 基底路徑、配置和主機。

呼叫 LinkGenerator 並指定一個位址。 執行下列兩個步驟來產生 URI:

  1. 將位址繫結至符合該位址的端點清單。
  2. 評估每個端點的 RoutePattern,直到找到符合所提供值的路由模式。 產生的輸出會與提供給連結產生器的其他 URI 組件合併並傳回。

LinkGenerator 提供的方法支援適用於任何位址類型的標準連結產生功能。 使用連結產生器的最便利方式是透過執行特定位址類型作業的擴充方法:

擴充方法 描述
GetPathByAddress 根據提供的值產生具有絕對路徑的 URI。
GetUriByAddress 根據提供的值產生絕對 URI。

警告

注意呼叫 LinkGenerator 方法的下列影響:

  • 使用 GetUri* 擴充方法,並注意應用程式組態不會驗證傳入要求的 Host 標頭。 如果傳入要求的 Host 標頭未經驗證,則可能將未受信任的要求輸入傳回檢視或頁面 URI 中的用戶端。 建議所有生產應用程式將其伺服器設定為驗證 Host 標頭是否為已知有效值。

  • 使用 LinkGenerator,並注意與 MapMapWhen 搭配使用的中介軟體。 Map* 會變更執行要求的基底路徑,這會影響連結產生的輸出。 所有的 LinkGenerator API 都允許指定基底路徑。 指定空基底路徑,以復原 Map* 對連結產生的影響。

中介軟體範例

在下列範例中,中介軟體會使用 LinkGenerator API 來建立列出市集產品的動作方法連結。 應用程式中的任何類別都可以使用連結產生器,方法是將它插入類別並呼叫 GenerateLink

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

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

路由範本

{} 內的權杖定義路由參數,這些參數會在路由相符時繫結。 路由區段中可以定義多個路由參數,但路由參數必須以常值分隔。 例如:

{controller=Home}{action=Index}

不是有效的路由,因為 {controller}{action} 之間沒有常值。 路由參數必須具有名稱,而且可能已指定其他屬性。

路由參數之外的常值文字 (例如,{id}) 和路徑分隔符號 / 必須符合 URL 中的文字。 文字比對不區分大小寫,並根據 URL 路徑的解碼標記法。 若要比對常值路由參數分隔符號 {},請重複字元來逸出分隔符號。 例如 {{}}

星號 * 或雙星號 **

  • 可作為路由參數的前置詞,以繫結至 URI 的其餘部分。
  • 稱為 catch-all 參數。 例如 blog/{**slug}
    • 比對開頭為 blog/ 的任何 URI,並在它之後具有任何值。
    • 下列 blog/ 值會指派給路由值 slug

警告

由於路由中的錯誤 (bug)catch-all 參數可能會錯誤比對路由。 受到此錯誤 (bug) 影響的應用程式具有下列特性:

  • catch-all 路由,例如 {**slug}"
  • catch-all 路由無法比對應該相符的要求。
  • 移除其他路由讓 catch-all 路由開始運作。

如需發生此錯誤 (bug) 的範例案例,請參閱 GitHub 錯誤 (bug) 1867716579

這個錯誤 (bug) 的加入修正包含在 .NET Core 3.1.301 SDK 和更新版本。 下列程式碼會設定修正此錯誤 (bug) 的內部參數:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

全部擷取參數也可以符合空字串。

當使用路由產生 URL (包括路徑分隔符號 / 字元) 時,catch-all 參數會逸出適當的字元。 例如,路由值為 { path = "my/path" } 的路由 foo/{*path} 會產生 foo/my%2Fpath。 請注意逸出的斜線。 若要反覆存取路徑分隔符號字元,請使用 ** 路由參數前置詞。 具有 { path = "my/path" } 的路由 foo/{**path} 會產生 foo/my/path

URL 模式嘗試擷取具有選擇性副檔名的檔案名稱時,具有其他考量。 以範本 files/{filename}.{ext?} 為例。 當 filenameext 都存在值時,就會填入這兩個值。 如果 URL 中只有 filename 的值存在,路由會比對,因為尾端 . 是選擇性的。 下列 URL 符合此路由:

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

路由參數可能有「預設值」,指定方法是在參數名稱之後指定預設值,並以等號 (=) 分隔。 例如,{controller=Home} 定義 Home 作為 controller 的預設值。 如果 URL 中沒有用於參數的任何值,則會使用預設值。 路由參數也可以設為選擇性,方法是在參數名稱結尾附加問號 (?)。 例如: id? 。 選擇性值與預設路由參數之間的差異如下:

  • 具有預設值的路由參數一律會產生值。
  • 選擇性參數只有在要求 URL 提供值時才具有值。

路由參數可能具有條件約束,這些條件約束必須符合與 URL 繫結的路由值。 在路由參數名稱後面新增 : 和條件約束名稱,以指定路由參數上的內嵌條件約束。 如果條件約束需要引數,這些引數會在條件約束名稱後面以括弧 (...) 括住。 您可以附加另一個 : 和條件約束名稱,以指定多個內嵌條件約束

條件約束名稱和引述會傳遞至 IInlineConstraintResolver 服務來建立 IRouteConstraint 的執行個體,以用於 URL 處理。 例如,路由範本 blog/{article:minlength(10)} 指定具有引數 10minlength 條件約束。 如需路由條件約束和架構所提供之條件約束清單的詳細資訊,請參閱路由條件約束一節。

路由參數也可能有參數轉換器。 參數轉換器會在產生連結和比對動作和頁面至 URL 時,轉換參數的值。 如同條件約束,參數轉換器可以藉由在路由參數名稱後面新增 : 和轉換器名稱,以內嵌方式新增至路由參數。 例如,路由範本 blog/{article:slugify} 會指定 slugify 轉換器。 如需參數轉換器的詳細資訊,請參閱參數轉換器一節。

下表示範範例路由範本及其行為:

路由範本 範例比對 URI 要求 URI
hello /hello 只比對單一路徑 /hello
{Page=Home} / 比對並將 Page 設定為 Home
{Page=Home} /Contact 比對並將 Page 設定為 Contact
{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
{controller=Home}/{action=Index}/{id?} /Products 對應至 Products 控制器和 Index 方法。 已忽略 id

使用範本通常是最簡單的路由方式。 條件約束和預設值也可以在路由範本外部指定。

複雜區段

複雜區段會以非窮盡方式,比對從右至左的常值分隔符號來處理。 例如,[Route("/a{b}c{d}")] 是複雜的區段。 複雜區段會以特定方式運作,必須了解才能成功使用這些區段。 本節中的範例示範當分隔符號文字未出現在參數值內時,複雜區段才真正正常運作的原因。 使用 RegEx,然後手動擷取更複雜的案例需要值。

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

這是路由使用範本執行的步驟摘要,/a{b}c{d} 和 URL 路徑 /abcd| 可用來協助視覺化演算法的運作方式:

  • 第一個常值,由右至左,是 c。 因此,/abcd 會從右搜尋,並尋找 /ab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/ab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 沒有剩餘的文字,也沒有剩餘的路由範本,所以這是相符的。

以下是使用相同範本 /a{b}c{d} 和 URL 路徑 /aabcd 的負案例範例。 | 可用來協助視覺化演算法的運作方式。 此案例與相同演算法所說明的不相符:

  • 第一個常值,由右至左,是 c。 因此,/aabcd 會從右搜尋,並尋找 /aab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/aab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /a|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 此時有剩餘的文字 a,但演算法已用盡路由範本來剖析,因此這不是相符項目。

由於比對演算法非窮盡

  • 它會比對每個步驟中可能最少的文字量。
  • 在參數值內出現分隔符號值的任何情況,都會導致不相符。

正則運算式可更充分地控制其比對行為。

窮盡比對,也稱為最大比對嘗試在符合 RegEx 模式的輸入文字中尋找最長的相符項目。 非窮盡比對,也稱為延遲比對,會在符合 RegEx 模式的輸入文字中搜尋最短的相符項目。

具有特殊字元的路由

使用特殊字元的路由可能會導致非預期的結果。 例如,請考慮使用下列動作方法的控制器:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id 包含下列編碼值時,可能會發生非預期的結果:

ASCII 已編碼
/ %2F
+

路由參數不一定會解碼 URL。 未來可能會解決此問題。 如需詳細資訊,請參閱這個 GitHub 問題

路由條件約束

路由條件約束執行時機是出現符合傳入 URL 的項目,並將 URL 路徑語彙基元化成路由值時。 路由條件約束通常會透過路由範本檢查相關聯的路由值,並針對值是否可接受做出 [True] 或 [False] 的決策。 某些路由條件約束會使用路由值以外的資料,以考慮是否可以路由要求。 例如,HttpMethodRouteConstraint 可以依據其 HTTP 指令動詞接受或拒絕要求。 條件約束可用於路由要求和連結產生。

警告

請勿針對輸入驗證使用條件約束。 如果條件約束用於輸入驗證,則不正確輸入會導致 404 找不到回應。 不正確輸入應該會產生 400 錯誤要求,並顯示適當的錯誤訊息。 路由條件約束會用來釐清類似的路由,而不是用來驗證特定路由的輸入。

下表示範範例路由條件約束及其預期行為:

條件約束 範例 範例相符項目 備註
int {id:int} 123456789, -123456789 符合任何整數
bool {active:bool} trueFALSE 比對 truefalse。 不區分大小寫
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm 比對不因文化特性而異中的有效 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 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

以冒號分隔的多個條件約束,可以套用至單一參數。 例如,下列條件約束會將參數限制在 1 或更大的整數值:

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

警告

驗證 URL 並轉換成 CLR 類型的路由條件約束一律使用不因文化特性而異。 例如,轉換成 CLR 類型 intDateTime。 這些條件約束假設 URL 無法當地語系化。 架構提供的路由條件約束不會修改路由值中儲存的值。 所有從 URL 剖析而來的路由值會儲存為字串。 例如,float 條件約束會嘗試將路由值轉換成浮點數,但轉換的值只能用來確認它可以轉換成浮點數。

條件約束中的規則運算式

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

規則運算式可以使用 regex(...) 路由條件約束來指定為內嵌條件約束。 MapControllerRoute 系列中的方法也會接受條件約束的物件常值。 如果使用該表單,字串值會解譯為規則運算式。

下列程式碼使用內嵌 RegEx 條件約束:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

下列程式碼會使用物件常值來指定 RegEx 條件約束:

app.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}$,請使用下列其中一項:

  • 將字串中提供的 \ 字元取代為 C# 來源檔案中的 \\ 字元,以逸出 \ 字串逸出字元。
  • 逐字字串常值

若要逸出路由參數分隔符號 {}[]、運算式中的字元,例如,{{}}[[]]。 下表顯示規則運算式及其逸出的版本:

規則運算式 逸出的規則運算式
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

路由中使用的規則運算式通常會以 ^ 字元開頭,並符合字串的起始位置。 運算式通常會以 $ 字元結尾,並符合字串結尾。 ^$ 字元可確保規則運算式符合整個路由參數值。 如果沒有 ^$ 個字元,規則運算式會比對字串內的任何子字串,這通常是不想要的。 下表提供範例,並說明它們符合或無法符合的原因:

運算式 String 比對 註解
[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 規則運算式

若要將參數限制為一組已知的可能值,請使用規則運算式。 例如,{action:regex(^(list|get|create)$)} 只會將 action 路由值與 listgetcreate 相符。 如果已傳入條件約束字典,字串 ^(list|get|create)$ 則是對等項目。 在不符合其中一個已知條件約束的條件約束字典中傳遞的條件約束也會被視為規則運算式。 在不符合其中一個已知條件約束的範本內傳遞的條件約束不會被視為規則運算式。

自訂路由條件約束

您可以實作 IRouteConstraint 介面來建立自訂路由條件約束。 IRouteConstraint 介面包含 Match,如果滿足條件約束,則會傳回 true,否則會傳回 false

自訂路由條件約束很少需要。 實作自訂路由條件約束之前,請考慮替代專案,例如模型繫結。

ASP.NET Core Constraints 資料夾提供建立條件約束的良好範例。 例如,GuidRouteConstraint

若要使用自訂 IRouteConstraint,路由條件約束類型必須向服務容器中的應用程式 ConstraintMap 註冊。 ConstraintMap 是一個目錄,它將路由限制式機碼對應到可驗證那些限制式的 IRouteConstraint 實作。 應用程式 ConstraintMap 可以在 Program.cs 中更新為 AddRouting 呼叫的一部分,或是直接使用 builder.Services.Configure<RouteOptions> 設定 RouteOptions。 例如:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上述條件約束會套用在下列程式碼中:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint 的實作可防止路由參數中使用 0

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[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 var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

上述 程式碼:

  • 防止路由 {id} 區段中的 0
  • 示範如何提供實作自訂條件約束的基本範例。 它不應該用於生產應用程式。

以下程式碼是防止處理包含 0id 的較佳方法:

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

    return Content(id);
}

上述程式碼在 NoZeroesRouteConstraint 方法上具有下列優點:

  • 它不需要自訂條件約束。
  • 當路由參數包含 0 時,它會傳回更具描述性的錯誤。

參數轉換器

參數轉換程式:

例如,具有 Url.Action(new { article = "MyTestArticle" }) 之路由模式 blog\{article:slugify} 中的自訂 slugify 參數轉換器,會產生 blog\my-test-article

請考慮下列 IOutboundParameterTransformer 實作:

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

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

若要在路由模式中使用參數轉換器,請使用 Program.cs 中的 ConstraintMap 進行設定:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core 架構會使用參數轉換器來轉換端點解析的 URI。 例如,參數轉換器會轉換用來比對 areacontrolleractionpage 的路由值:

app.MapControllerRoute(
    name: "default",
    pattern: "{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 頁面。 如需詳細資訊,請參閱控制器中的路由

URL 產生流程會從呼叫 LinkGenerator.GetPathByAddress 或類似的方法開始。 方法會提供位址、一組路由值,以及選擇性地從 HttpContext 取得目前要求的相關資訊。

第一個步驟是使用位址來解析一組候選端點,其 IEndpointAddressScheme<TAddress> 符合位址的類型。

一旦位址配置找到一組候選項目之後,端點會逐一排序並反復處理,直到 URL 產生作業成功為止。 URL 產生不會檢查模棱兩可,傳回的第一個結果會是最終結果。

使用記錄對 URL 產生進行疑難排解

疑難排解 URL 產生的第一個步驟是將 Microsoft.AspNetCore.Routing 的記錄層級設定為 TRACELinkGenerator 記錄其處理的許多詳細資料,這對於疑難排解問題很有用。

如需 URL 產生的詳細資訊,請參閱 URL 產生參考

地址

位址是 URL 產生中用來將呼叫繫結至連結產生器至一組候選端點的概念。

位址是預設隨附兩個實作的可延伸概念:

  • 使用端點名稱 (string) 作為位址:
    • 提供與 MVC 路由名稱類似的功能。
    • 使用 IEndpointNameMetadata 中繼資料類型。
    • 針對所有已註冊端點的中繼資料解析提供的字串。
    • 如果多個端點使用相同的名稱,則會在啟動時擲回例外狀況。
    • 建議在控制器和 Razor 頁面外部使用一般用途。
  • 使用路由值 (RouteValuesAddress) 作為位址:
    • 提供與控制器和 Razor 頁面舊版 URL 產生類似的功能。
    • 擴充和偵錯非常複雜。
    • 提供 IUrlHelper、標籤協助程式、HTML 協助程式、動作結果等所使用的實作。

位址配置的角色是依任意準則在位址與相符端點之間建立關聯:

  • 端點名稱配置會執行基本字典查閱。
  • 路由值配置具有複雜的集合演算法子集。

環境值和明確值

從目前的要求,路由會存取目前要求的路由值 HttpContext.Request.RouteValues。 與目前要求相關聯的值稱為環境值。 為了清楚起見,文件將傳入方法的路由值稱為明確值

下列範例顯示環境值和明確值。 它提供目前要求的環境值和明確值:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上述 程式碼:

下列程式碼只提供明確值,而且沒有環境值:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

上述方法會傳回 /Home/Subscribe/17

WidgetController 中的下列程式碼會傳回 /Widget/Subscribe/17

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

下列程式碼會從目前要求中的環境值和明確值提供控制器:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

在上述程式碼中:

  • 傳回 /Gadget/Edit/17
  • Url 取得 IUrlHelper
  • Action 會產生具有動作方法絕對路徑的 URL。 URL 包含指定的 action 名稱和 route 值。

下列程式碼提供來自目前要求的環境值和明確值:

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

        // ...
    }
}

上述程式碼會將編輯 Razor 頁面包含下列頁面指示詞時,將 url 設定為 /Edit/17

@page "{id:int}"

如果 [編輯] 頁面不包含 "{id:int}" 路由範本,url 則為 /Edit?id=17

MVC IUrlHelper 的行為除了此處所述的規則之外,也會新增一層複雜度:

  • IUrlHelper 一律提供目前要求的路由值作為環境值。
  • 除非開發人員覆寫,否則 IUrlHelper.Action 一律會將目前 action 值和 controller 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會將目前的 page 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會覆寫目前 handler 路由值,其中 null 做為明確值。

使用者通常對環境值的行為詳細資料感到驚訝,因為 MVC 似乎沒有遵循自己的規則。 基於歷程記錄和相容性考慮,某些路由值,例如 actioncontrollerpagehandler 有自己的特殊案例行為。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage 所提供的對等功能會複製這些 IUrlHelper 的異常,以取得相容性。

URL 產生流程

找到一組候選端點之後,URL 產生演算法:

  • 反復處理端點。
  • 傳回第一個成功的結果。

此程式中的第一個步驟稱為路由值無效。 路由值失效是路由決定應該使用環境值和應忽略哪些路由值的流程。 系統會考慮每個環境值,並結合明確值或忽略。

思考環境值角色的最佳方式是,他們嘗試在一些常見情況下儲存應用程式開發人員輸入。 傳統上,環境值有用的案例與 MVC 相關:

  • 連結至相同控制器中的另一個動作時,不需要指定控制器名稱。
  • 連結至相同區域中的另一個控制器時,不需要指定區功能變數名稱。
  • 連結至相同的動作方法時,不需要指定路由值。
  • 連結至應用程式的另一個部分時,您不想傳遞在應用程式該部分沒有意義的路由值。

對傳回 nullLinkGeneratorIUrlHelper 的呼叫通常是因為無法了解路由值無效所造成。 藉由明確指定更多路由值來針對路由值失效進行疑難排解,以查看這是否可解決問題。

路由值失效的運作方式是假設應用程式的 URL 配置是階層式,而階層是由左至右形成。 請考慮基本控制器路由範本 {controller}/{action}/{id?},以直覺方式了解實際運作方式。 值變更會使所有出現在右邊的路由值失效。 這反映了階層的假設。 如果應用程式有 id 的環境值,則作業會為 controller 指定不同的值:

  • id 不會重複使用,因為 {controller} 位於 {id?} 的左邊。

示範此原則的一些範例:

  • 如果明確值包含 id 的值,則會忽略 id 的環境值。 可以使用 controlleraction 的環境值。
  • 如果明確值包含 action 的值,則會忽略 action 的任何環境值。 可以使用 controller 的環境值。 如果 action 的明確值與 action 的環境值不同,則不會使用 id 值。 如果 action 的明確值與 action 的環境值相同,則可以使用 id 值。
  • 如果明確值包含 controller 的值,則會忽略 controller 的任何環境值。 如果 controller 的明確值與 controller 的環境值不同,則不會使用 actionid 值。 如果 controller 的明確值與 controller 的環境值相同,則可以使用 actionid 值。

透過屬性路由和專用傳統路由的存在,此流程會進一步複雜。 控制器傳統路由,例如 {controller}/{action}/{id?} 使用路由參數指定階層。 針對專用傳統路由屬性路由控制器和 Razor 頁面:

  • 路由值有階層。
  • 它們不會出現在範本中。

在這些情況下,URL 產生會定義必要值概念。 控制器和 Razor 頁面所建立的端點具有指定的必要值,可讓路由值失效運作。

路由值無效演算法詳細資料:

  • 必要的值名稱會與路由參數結合,然後從左至右進行處理。
  • 針對每個參數,會比較環境值和明確值:
    • 如果環境值和明確值相同,流程會繼續。
    • 如果環境值存在且明確值不存在,則產生 URL 時會使用環境值。
    • 如果環境值不存在且明確值存在,請拒絕環境值和所有後續的環境值。
    • 如果環境值和明確值存在,且兩個值不同,請拒絕環境值和所有後續的環境值。

此時,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

選擇性路由參數順序

選擇性路由參數必須位在所有必要的路由參數和常值之後。 在下列程式碼中, idname 參數必須位於 color 參數之後:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

路由值不正確問題

下列程式碼顯示路由不支援的 URL 產生配置範例:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

在上述程式碼中,會使用 culture 路由參數進行當地語系化。 想要讓 culture 參數一律接受為環境值。 不過,由於所需值的運作方式,culture 參數不會接受為環境值:

  • "default" 路由範本中,culture 路由參數位於 controller 的左邊,因此 controller 的變更不會使 culture無效。
  • "blog" 路由範本中,culture 路由參數會視為 controller 的右邊,其會出現在必要值中。

使用 LinkParser 剖析 URL 路徑

LinkParser 類別會將剖析 URL 路徑的支援新增至一組路由值。 ParsePathByEndpointName 方法會採用端點名稱和 URL 路徑,並傳回從 URL 路徑擷取的一組路由值。

在下列範例控制器中,GetProduct 動作會使用 api/Products/{id} 的路由範本,並具有 GetProductName

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

在相同的控制器類別中,AddRelatedProduct 動作需要 URL 路徑,pathToRelatedProduct,其可作為查詢字串參數提供:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

在上述範例中,AddRelatedProduct 動作會從 URL 路徑擷取 id 路由值。 例如,使用 /api/Products/1 的 URL 路徑,relatedProductId 值會設定為 1。 此方法可讓 API 的用戶端在參考資源時使用 URL 路徑,而不需要了解這類 URL 的結構。

設定端點中繼資料

下列連結提供如何設定端點中繼資料的資訊:

搭配 RequireHost 路由中的主機比對

RequireHost 會將條件約束套用至需要指定主機的路由。 RequireHost 或 [Host] 參數可以是:

  • 主機:www.domain.com,符合任何連接埠 www.domain.com
  • 具有萬用字元的主機:*.domain.com、比對任何連接埠上的 www.domain.comsubdomain.domain.comwww.subdomain.domain.com
  • 連接埠:*:5000,符合連接埠 5000 與任何主機。
  • 主機和連接埠:www.domain.com:5000*.domain.com:5000,符合主機和連接埠。

可以使用 RequireHost[Host] 來指定多個參數。 條件約束會比對任何參數有效的主機。 例如,[Host("domain.com", "*.domain.com")] 符合 domain.comwww.domain.comsubdomain.domain.com

下列程式碼會使用 RequireHost 在路由上要求指定的主機:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

下列程式碼會使用控制器上的 [Host] 屬性來要求任何指定的主機:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 屬性同時套用至控制器和動作方法時:

  • 會使用動作上的屬性。
  • 控制器屬性會被忽略。

警告

依賴於主機標頭的 API (例如 HttpRequest.HostRequireHost) 可能會受到用戶端的詐騙。

若要防止主機和連接埠詐騙,請使用下列其中一種方式:

路由群組

MapGroup 擴充方法可協助組織具有常見前置詞的端點群組。 其可減少重複的程式碼,並允許使用單一呼叫方法 (例如 RequireAuthorizationWithMetadata,其可新增端點中繼資料) 來自訂整個端點群組。

例如,下列程式碼會建立兩個類似的端點群組:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

在此案例中,您可以在 201 Created 結果中使用 Location 標頭的相對位址:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

第一組端點只會比對以 /public/todos 為前置詞且不需要任何驗證即可存取的要求。 第二組端點只會比對以 /private/todos 為前置詞且需要驗證的要求。

QueryPrivateTodos端點篩選處理站是本機函式,可修改路由處理常式的 TodoDb 參數,以允許存取及儲存私人 Todo 資料。

路由群組也支援具有路由參數和條件約束的巢狀群組和複雜前置詞模式。 在下列範例中,對應至 user 群組的路由處理常式可以擷取外部群組前置詞中定義的 {org}{group} 路由參數。

前置詞也可能是空的。 這適用於將端點中繼資料或篩選條件新增至一組端點,而不需要變更路由模式。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

將篩選條件或中繼資料新增至群組的行為,與將篩選條件或中繼資料個別新增至每個端點,再新增任何可能已新增至內部群組或特定端點的額外篩選條件或中繼資料的方式相同。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

在上述範例中,即使已新增第二個要求,外部篩選條件也會在內部篩選條件之前記錄傳入要求。 由於篩選條件已套用至不同的群組,因此彼此相對的新增順序並不重要。 如果套用至相同的群組或特定端點,則篩選條件的新增順序就很重要。

/outer/inner/ 的要求會記錄下列內容:

/outer group filter
/inner group filter
MapGet filter

路由的效能指引

當應用程式發生效能問題時,通常會懷疑路由有問題。 路由的原因是控制器和 Razor 頁面等架構會報告架構在其記錄訊息中花費的時間量。 當控制器報告的時間與要求的總時間之間有顯著差異時:

  • 開發人員會將其應用程式程式碼排除為問題的來源。
  • 通常假設路由是原因。

路由是使用數千個端點測試的效能。 一般應用程式不太可能因為太大而遇到效能問題。 路由效能緩慢最常見的根本原因是自訂中介軟體行為不良。

下列程式碼範例示範縮小延遲來源的基本技巧:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

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

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

app.UseRouting();

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

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

app.UseAuthorization();

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

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

app.MapGet("/", () => "Timing Test.");

計時路由:

  • 將每個中介軟體與上述程式碼中顯示的計時中介軟體複本交錯。
  • 新增唯一識別碼,以將計時資料與程式碼相互關聯。

這是一種基本方式,可以在延遲顯著時縮小延遲,例如,超過 10ms。 從 Time 1 減去 Time 2 會報告 UseRouting 中介軟體內所花費的時間。

下列程式碼會使用更精簡的方法來處理上述計時程式碼:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

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

app.UseRouting();

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

app.UseAuthorization();

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

app.MapGet("/", () => "Timing Test.");

潛在昂貴的路由功能

下列清單提供相較於基本路由範本相對昂貴之路由功能的一些見解:

  • 規則運算式:可以撰寫複雜的規則運算式,或有少量輸入的長時間執行時間。
  • 複雜區段 ({x}-{y}-{z}):
    • 比剖析一般 URL 路徑區段更昂貴。
    • 導致配置更多子字串。
  • 同步資料存取:許多複雜的應用程式在其路由中具有資料庫存取權。 使用擴充點,例如 MatcherPolicyEndpointSelectorContext,這是非同步。

大型路由表的指引

根據預設,ASP.NET Core 會使用路由演算法來交換 CPU 時間的記憶體。 這有一個良好的效果,路由比對時間只取決於路徑的長度,而不是路由數目。 不過,在某些情況下,當應用程式有大量的路由 (數千個),而且路由中有大量的變數前置詞時,這種方法可能會有問題。 例如,如果路由在路由的早期區段中具有參數,例如 {parameter}/some/literal

除非下列情況,否則應用程式不太可能遇到此問題的情況:

  • 此模式在應用程式中有許多路由。
  • 應用程式中有許多路由。

如何判斷應用程式是否正在發生大型路由表問題

  • 有兩個徵兆需要尋找:
    • 應用程式在第一個要求上啟動的速度很慢。
      • 請注意,這是必要項目,但並不足夠。 還有其他許多非路由問題,可能會導致應用程式啟動緩慢。 檢查下列條件,以正確判斷應用程式是否正在發生這種情況。
    • 應用程式會在啟動期間耗用大量的記憶體,而記憶體傾印會顯示大量 Microsoft.AspNetCore.Routing.Matching.DfaNode 執行個體。

如何解決此問題

有數種可適用於路由的技術和最佳化,可大幅改善此案例:

  • 視需要將路由條件約束套用至參數,例如 {parameter:int}{parameter:guid}{parameter:regex(\\d+)} 等。
    • 這可讓路由演算法在內部最佳化用於比對的結構,並大幅減少所使用的記憶體。
    • 在絕大多數情況下,這足以回到可接受的行為。
  • 變更路由以將參數移至範本中的稍後區段。
    • 這樣可減少可能「路徑」的數目,以符合指定路徑的端點。
  • 使用動態路由,並動態執行控制器/頁面的對應。
    • 這可以使用 MapDynamicControllerRouteMapDynamicPageRoute 來達成。

路由之後尋找中介軟體的最短路徑

當路由符合端點時,通常會讓其餘中介軟體管線在叫用端點邏輯之前執行。 服務可以篩選出管線早期已知的要求來減少資源使用量。 使用 ShortCircuit 擴充方法可讓路由立即叫用端點邏輯,然後結束要求。 例如,指定的路由可能不需要通過驗證或 CORS 中介軟體。 下列範例會尋找符合 /short-circuit 路由要求的最短路徑:

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) 方法可以選擇性地取得狀態碼。

使用 MapShortCircuit 方法,將 URL 前置詞的參數陣列傳遞給多個路由,以一次設定多個路由的最短路徑。 例如,瀏覽器和 Bot 通常會探查伺服器的已知路徑,例如 robots.txtfavicon.ico。 如果應用程式沒有這些檔案,則一行程式碼即可設定這兩個路由:

app.MapShortCircuit(404, "robots.txt", "favicon.ico");

MapShortCircuit 會傳回 IEndpointConventionBuilder,以便將主機篩選等其他路由條件約束新增至其中。

ShortCircuitMapShortCircuit 方法不會影響在 UseRouting 之前放置的中介軟體。 嘗試搭配具有 [Authorize][RequireCors] 中繼資料的端點使用這些方法,會導致要求失敗並產生 InvalidOperationException。 此中繼資料會由 [Authorize][EnableCors] 屬性或 RequireCorsRequireAuthorization 方法套用。

若要查看縮短中介軟體的效果,請將 appsettings.Development.json 中的 "Microsoft" 記錄類別設定為 "Information":

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

執行下列程式碼:

var app = WebApplication.Create();

app.UseHttpLogging();

app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");

app.Run();

下列範例來自執行 / 端點所產生的主控台記錄。 其中包含記錄中介軟體的輸出:

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Wed, 03 May 2023 21:05:59 GMT
      Server: Kestrel
      Alt-Svc: h3=":5182"; ma=86400
      Transfer-Encoding: chunked

下列範例是執行 /short-circuit 端點。 它沒有任何記錄中介軟體,因為中介軟體已縮短:

info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
      The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
      The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.

程式庫作者的指引

本節包含程式庫作者建置在路由之上的指引。 這些詳細資料旨在確保應用程式開發人員有良好的使用程式庫和架構來擴充路由的體驗。

定義端點

若要建立使用路由進行 URL 比對的架構,請先定義建置在 UseEndpoints 之上的使用者體驗。

DO 建置在 IEndpointRouteBuilder 之上。 這可讓使用者與其他 ASP.NET Core 功能撰寫架構,而不會造成混淆。 每個 ASP.NET Core 範本都包含路由。 假設使用者有路由存在且熟悉。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DO 會從實作 IEndpointConventionBuilderMapMyFramework(...) 呼叫傳回密封具體型別。 大部分的架構 Map... 方法都遵循此模式。 IEndpointConventionBuilder 介面:

  • 允許撰寫中繼資料。
  • 以各種擴充方法為目標。

宣告您自己的型別可讓您將自己的架構特定功能新增至建立器。 可以包裝架構宣告的建立器,並將呼叫轉接給它。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

考慮撰寫您自己的 EndpointDataSourceEndpointDataSource 是宣告和更新端點集合的低層級基本類型。 EndpointDataSource 是控制器和 Razor 頁面所使用的強大 API。 如需詳細資訊,請參閱動態端點路由

路由測試具有非更新資料來源基本範例

考慮實作 GetGroupedEndpoints。 這可完整控制執行群組慣例,以及群組端點上的最終中繼資料。 例如,這可讓自訂 EndpointDataSource 實作執行端點篩選新增至群組。

根據預設,請勿嘗試註冊 EndpointDataSource。 要求使用者在 UseEndpoints 中註冊您的架構。 路由的理念是預設不會包含任何項目,而且 UseEndpoints 是註冊端點的位置。

建立路由整合式中介軟體

考慮將中繼資料型別定義為介面。

DO 可讓您在類別和方法上使用中繼資料型別作為屬性。

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

控制器和 Razor 頁面等架構支援將中繼資料屬性套用至型別和方法。 如果您宣告中繼資料型別:

  • 將它們設為可存取的屬性
  • 大部分的使用者都熟悉套用屬性。

將中繼資料型別宣告為介面會新增另一層彈性:

  • 介面是可組合的。
  • 開發人員可以宣告自己結合多個原則的型別。

DO 可讓您覆寫中繼資料,如下列範例所示:

[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() { }
}

遵循這些指導方針的最佳方式是避免定義標記中繼資料

  • 不要只尋找中繼資料型別是否存在。
  • 在中繼資料上定義屬性,並檢查屬性。

中繼資料集合是排序的,並支援依優先順序覆寫。 在控制器的情況下,動作方法上的中繼資料最具體。

DO 讓中介軟體在不使用路由的情況下使用:

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

// Your framework
app.MapMyFramework(...).RequireAuthorization();

作為此指導方針的範例,請考慮 UseAuthorization 中介軟體。 授權中介軟體可讓您傳入後援原則。 如果指定後援原則,則適用於兩者:

  • 不含指定原則的端點。
  • 不符合端點的要求。

這可讓授權中介軟體在路由內容之外很有用。 授權中介軟體可用於傳統中介軟體程式設計。

偵錯診斷

如需詳細的路由診斷輸出,請將 Logging:LogLevel:Microsoft 設定為 Debug。 在開發環境中,在 appsettings.Development.json 中設定記錄層級:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

其他資源

路由負責比對連入 HTTP 要求,並將這些要求分派至應用程式的可執行端點。 端點是應用程式的可執行檔要求處理程式碼單位。 端點會在應用程式中定義,並在應用程式啟動時設定。 端點比對程式可以從要求的 URL 擷取值,並提供這些值以供要求處理。 使用來自應用程式的端點資訊,路由也能夠產生對應至端點的 URL。

應用程式可以使用下列方式設定路由:

本文涵蓋路由 ASP.NET Core 低層級詳細資料。 如需設定路由的資訊:

路由的基本概念

下列程式碼顯示路由的基本範例:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上述範例包含使用 MapGet 方法的單一端點:

  • 當 HTTP GET 要求傳送至根 URL / 時:
    • 要求委派會執行。
    • Hello World! 會寫入 HTTP 回應。
  • 如果要求方法不是 GET 或根 URL 不是 /,則不會比對路由,而且會傳回 HTTP 404。

路由使用一對中介軟體,由 UseRoutingUseEndpoints 註冊:

  • UseRouting 會將路由比對新增至中介軟體管線。 此中介軟體會查看應用程式中定義的端點集,並根據要求選取最佳端點比對
  • UseEndpoints 將端點執行新增至中介軟體管線。 它會執行與所選端點相關聯的委派。

應用程式通常不需要呼叫 UseRoutingUseEndpointsWebApplicationBuilder 設定中介軟體管線,此管線會包裝使用 UseRoutingUseEndpointsProgram.cs 中新增的中介軟體。 不過,應用程式可以藉由明確呼叫這些方法,變更 UseRoutingUseEndpoints 執行的順序。 例如,下列程式碼會明確呼叫 UseRouting

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

在上述程式碼中:

  • app.Use 的呼叫會註冊在管線開頭執行的自訂中介軟體。
  • 呼叫 UseRouting 會設定路由比對中介軟體,以在自訂中介軟體之後執行。
  • MapGet 註冊的端點會在管線結尾執行。

如果上述範例未包含對 UseRouting 的呼叫,則自訂中介軟體會在路由比對中介軟體之後執行。

端點

MapGet 方法可用來定義端點。 端點可以是:

  • 選取,方法是比對 URL 和 HTTP 方法。
  • 執行,透過執行委派。

應用程式可以比對和執行的端點會在 UseEndpoints 中設定。 例如,MapGetMapPost類似的方法,將要求委派連接到路由系統。 其他方法可用來將 ASP.NET Core 架構功能連線至路由系統:

下列範例示範使用更複雜的路由範本進行路由:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

字串 /hello/{name:alpha}路由範本。 路由範本可用來設定端點的比對方式。 在此情況下,範本會比對:

  • URL,例如 /hello/Docs
  • 開頭為 /hello/ 後面接著一連串字母字元的任何 URL 路徑。 :alpha 套用只符合字母字元的路由條件約束。 本文稍後會說明路由條件約束

URL 路徑的第二個區段 {name:alpha}

下列範例顯示具有健康狀態檢查和授權的路由:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上述範例示範如何:

  • 授權中介軟體可以搭配路由使用。
  • 端點可用來設定授權行為。

MapHealthChecks 呼叫會新增健康情況檢查端點。 鏈結 RequireAuthorization 至此呼叫會將授權原則附加至端點。

呼叫 UseAuthenticationUseAuthorization 以新增驗證和授權中介軟體。 這些中介軟體會放在 UseRoutingUseEndpoints 之間,讓它們能夠:

  • 查看 UseRouting 所選取的端點。
  • UseEndpoints 分派至端點之前,請先套用授權原則。

端點中繼資料

在上述範例中,有兩個端點,但只有健康情況檢查端點已附加授權原則。 如果要求符合健康情況檢查端點 /healthz,則會執行授權檢查。 這示範端點可以附加額外的資料。 此額外資料稱為端點中繼資料

  • 中繼資料可以透過路由感知中介軟體來處理。
  • 中繼資料可以是任何 .NET 類型。

路由概念

路由系統會藉由新增功能強大的端點概念,以建置在中介軟體管線之上。 端點代表應用程式功能單位,這些單位在路由、授權和任何數目 ASP.NET Core 的系統方面彼此不同。

ASP.NET Core 端點定義

ASP.NET Core 端點為:

下列程式碼示範如何擷取和檢查符合目前要求的端點:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

如果選取端點,可以從 HttpContext 擷取。 可以檢查其屬性。 端點物件是不可變的,在建立之後無法修改。 最常見的端點類型是 RouteEndpointRouteEndpoint 包含允許路由系統選取的資訊。

在上述程式碼中,app.Use 設定內嵌中介軟體

下列程式碼顯示,視管線中呼叫 app.Use 的位置而定,可能沒有端點:

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

app.UseRouting();

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

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

app.UseEndpoints(_ => { });

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

上述範例會新增 Console.WriteLine 語句,以顯示是否已選取端點。 為了清楚起見,此範例會將顯示名稱指派給提供的 / 端點。

上述範例也包含呼叫 UseRoutingUseEndpoints,以完全控制這些中介軟體在管線內執行時間。

執行具有 / URL 的程式碼會顯示:

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

使用任何其他 URL 執行此程式碼會顯示:

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

此輸出示範:

  • 呼叫 UseRouting 之前,端點一律為 null。
  • 如果找到相符專案,則端點在 UseRoutingUseEndpoints 之間為非 null。
  • 找到相符項目時,UseEndpoints 中介軟體是終端。 本文稍後會定義終端中介軟體
  • 只有在找不到相符項目時,才會執行 UseEndpoints 後的中介軟體。

UseRouting 中介軟體會使用 SetEndpoint 方法來將端點附加至目前的內容。 您可以使用自訂邏輯取代 UseRouting 中介軟體,但仍可取得使用端點的權益。 端點是低層級基本類型,例如中介軟體,不會與路由實作結合。 大部分的應用程式不需要以自訂邏輯取代 UseRouting

UseEndpoints 中介軟體的設計目的是與 UseRouting 中介軟體搭配使用。 執行端點的核心邏輯並不複雜。 使用 GetEndpoint 來擷取端點,然後叫用其 RequestDelegate 屬性。

下列程式碼示範中介軟體如何影響或回應路由:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

上述範例示範兩個重要概念:

  • 中介軟體可以在 UseRouting 之前執行,以修改路由運作的資料。
  • 中介軟體可以在 UseRoutingUseEndpoints 之間執行,以在執行端點之前處理路由的結果。
    • UseRoutingUseEndpoints之間執行的中介軟體:
      • 通常會檢查中繼資料以了解端點。
      • 通常會依照 UseAuthorizationUseCors 來完成安全性決策。
    • 中介軟體和中繼資料的組合可讓您設定每個端點的原則。

上述程式碼示範支援每個端點原則的自訂中介軟體範例。 中介軟體會將敏感性資料存取權的稽核記錄寫入主控台。 中介軟體可以設定為使用 RequiresAuditAttribute 中繼資料端點稽核。 此範例示範加入宣告模式,其中只會稽核標示為敏感性的端點。 您可以反向定義此邏輯,例如稽核未標示為安全的所有內容。 端點中繼資料系統具有彈性。 此邏輯可以設計成任何適合使用案例的方式。

上述範例程式碼旨在示範端點的基本概念。 此範例不適用於生產環境稽核記錄中介軟體的更完整版本會:

  • 登入檔案或資料庫。
  • 包含詳細資料,例如使用者、IP 位址、敏感性端點的名稱等。

稽核原則中繼資料 RequiresAuditAttribute 定義為 Attribute,以便更輕鬆地與控制器和 SignalR 等類別架構搭配使用。 使用 路由至程式碼 時:

  • 中繼資料會與產生器 API 連結。
  • 以類別為基礎的架構會在建立端點時,包含對應方法和類別上的所有屬性。

中繼資料類型的最佳做法是將它們定義為介面或屬性。 介面和屬性允許重複使用程式碼。 中繼資料系統具有彈性,而且不會施加任何限制。

比較終端中介軟體與路由

下列範例示範終端中介軟體和路由:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: 顯示的中介軟體樣式為終端中介軟體。 它稱為終端中介軟體,因為它會執行比對作業:

  • 上述範例中的比對作業針對中介軟體是 Path == "/",針對路由是 Path == "/Routing"
  • 比對成功時,它會執行一些功能並傳回,而不是叫用 next 中介軟體。

它稱為終端中介軟體,因為它會終止搜尋、執行某些功能,然後傳回。

下列清單會比較終端中介軟體與路由:

  • 這兩種方法都允許終止處理管線:
    • 中介軟體會傳回而不是叫用 next 來終止管線。
    • 端點一律為終端。
  • 終端中介軟體允許將中介軟體放置在管線中的任意位置:
  • 終端中介軟體可讓任意程式碼判斷中介軟體何時符合:
    • 自訂路由比對程式碼可能很詳細且難以正確撰寫。
    • 路由為一般應用程式提供直接的解決方案。 大部分的應用程式都不需要自訂路由比對程式碼。
  • 端點介面與中介軟體,例如 UseAuthorizationUseCors
    • 使用具有 UseAuthorizationUseCors 的終端中介軟體需要手動與授權系統互動。

端點定義兩者:

  • 處理要求的委派。
  • 任意中繼資料的集合。 中繼資料是用來根據附加至每個端點的原則和組態來實作跨領域考量。

終端中介軟體可以是有效的工具,但可能需要:

  • 大量的程式碼撰寫和測試。
  • 手動與其他系統整合,以達到所需的彈性層級。

在撰寫終端中介軟體之前,請考慮與路由整合。

MapMapWhen 整合的現有終端中介軟體通常可以轉換成路由感知端點。 MapHealthChecks 示範路由器軟體的模式:

下列程式碼示範如何使用 MapHealthChecks

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

上述範例顯示為何傳回建立器物件很重要。 傳回建立器物件可讓應用程式開發人員設定原則,例如端點的授權。 在此範例中,健康情況檢查中介軟體與授權系統沒有直接整合。

中繼資料系統是針對使用終端中介軟體的擴充性建立者所遇到的問題而建立的。 每個中介軟體實作自己的與授權系統的整合是有問題的。

URL 比對

  • 這是路由比對傳入要求與端點的流程。
  • 以 URL 路徑和標頭中的資料為基礎。
  • 可以擴充以考慮要求中的任何資料。

當路由中介軟體執行時,它會設定 Endpoint,並將值路由至目前要求 HttpContext 上的要求功能

在路由中介軟體可以檢查端點並採取動作之後,中介軟體執行。 例如,授權中介軟體可以詢問授權原則的端點中繼資料集合。 執行要求處理管線中的所有中介軟體之後,會叫用所選端點的委派。

端點路由中的路由系統負責制定所有分派決策。 因為中介軟體會根據選取的端點套用原則,因此請務必:

  • 任何可能影響分派或安全性原則應用的決定,都會在路由系統內進行。

警告

為了回溯相容性,當執行 Controller 或 Razor頁面端點委派時,RouteContext.RouteData 的屬性會根據到目前為止所執行的要求處理,設定為適當的值。

RouteContext 類型將在未來的版本中標示為過時:

  • RouteData.Values 移轉至 HttpRequest.RouteValues
  • 移轉 RouteData.DataTokens 以從端點中繼資料擷取 IDataTokensMetadata

URL 比對會在一組可設定的階段中運作。 在每個階段中,輸出是一組相符項目。 下一個階段可以進一步縮小一組相符項目。 路由實作不保證相符端點的處理順序。 所有可能的相符項目都會一次處理。 URL 比對階段會依下列順序進行。 ASP.NET Core:

  1. 針對一組端點及其路由範本處理 URL 路徑,並收集所有相符項目。
  2. 採用上述清單,並移除套用路由條件約束失敗的相符項目。
  3. 採用上述清單,並移除失敗 MatcherPolicy 執行個體集的相符項目。
  4. 使用 EndpointSelector 從上述清單做出最終決定。

端點清單會根據下列項目排定優先順序:

每個階段都會處理所有相符的端點,直到到達 EndpointSelector 為止。 EndpointSelector 是最後階段。 它會從相符項目中選擇最高的優先順序端點作為最佳相符項目。 如果其他相符項目具有與最佳相符項目相同的優先順序,則會擲回模棱兩可的比對例外狀況。

路由優先順序是根據更具體路由範本來計算,其優先順序較高。 例如,請考慮範本 /hello/{message}

  • 兩者都符合 URL 路徑 /hello
  • /hello 更具體,因此優先順序較高。

一般而言,路由優先順序會針對實際使用的 URL 配置類型,選擇最相符的作業。 只有在需要時才使用 Order 以避免模棱兩可。

由於路由所提供的擴充性種類,路由系統無法事先計算模棱兩可的路由。 請考慮路由範本 /{message:alpha}/{message:int} 等範例:

  • alpha 條件約束只符合字母字元。
  • int 條件約束只符合數字。
  • 這些範本具有相同的路由優先順序,但兩者都沒有相符的單一 URL。
  • 如果路由系統在啟動時回報模棱兩可的錯誤,則會封鎖此有效的使用案例。

警告

UseEndpoints 內的作業順序不會影響路由的行為,但有一個例外狀況。 MapControllerRouteMapAreaRoute 會根據叫用的順序,自動將訂單值指派給其端點。 這會模擬控制器的長期行為,而沒有路由系統提供與舊版路由實作相同的保證。

ASP.NET Core 中的端點路由:

  • 沒有路由的概念。
  • 不提供訂購保證。 所有端點都會一次處理。

路由範本優先順序和端點選取順序

路由範本優先順序是一個系統,會根據路由範本的特定程度,為每個路由範本指派值。 路由範本優先順序:

  • 避免在常見情況下調整端點的順序。
  • 嘗試比對路由行為的常識預期。

例如,請考慮範本 /Products/List/Products/{id}。 對於 URL 路徑 /Products/List,可以合理地假設 /Products/List/Products/{id} 更相符。 這可運作,因為常值區段 /List 的優先順序會比參數區段 /{id} 更好。

優先順序運作方式的詳細資料會與如何定義路由範本:

  • 具有更多區段的範本會被視為更具體。
  • 具有常值文字的區段會比參數區段更具體。
  • 具有條件約束的參數區段會被視為比不具條件的參數區段更具體。
  • 複雜區段會被視為具有條件約束的參數區段。
  • Catch-all 參數是最不具體的。 如需 catch-all 路由的重要資訊,請參閱路由範本一節中的 catch-all

URL 產生概念

URL 產生:

  • 這是路由可以根據一組路由值建立 URL 路徑的流程。
  • 允許在端點與存取端點的 URL 之間進行邏輯分隔。

端點路由包含 LinkGenerator API。 LinkGenerator 是單一資料庫,可從 DI 取得。 LinkGenerator API 可以在執行要求的內容之外使用。 Mvc.IUrlHelper 和依賴 IUrlHelper 的情節,例如標籤協助程式、HTML 協助程式,以及動作結果,請在內部使用 LinkGenerator API 來提供連結產生功能。

連結產生器背後支援的概念為「位址」和「位址配置」。 位址配置可讓您判斷應考慮用於連結產生的端點。 例如,路由名稱和路由值情節,許多使用者都熟悉來自控制器,Razor 頁面會實作為位址配置。

連結產生器可以透過下列擴充方法連結至控制器和 Razor 頁面:

這些方法的多載會接受包含 HttpContext 的引數。 這些方法的功能相當於 Url.ActionUrl.Page,但提供額外的彈性和選項。

GetPath* 方法最類似 Url.ActionUrl.Page,因為它們會產生包含絕對路徑的 URI。 GetUri* 方法一律會產生包含配置和主機的絕對 URI。 接受 HttpContext 的方法會在執行要求的內容中產生 URI。 除非覆寫,否則會使用來自執行要求的環境路由值、URL 基底路徑、配置和主機。

呼叫 LinkGenerator 並指定一個位址。 執行下列兩個步驟來產生 URI:

  1. 將位址繫結至符合該位址的端點清單。
  2. 評估每個端點的 RoutePattern,直到找到符合所提供值的路由模式。 產生的輸出會與提供給連結產生器的其他 URI 組件合併並傳回。

LinkGenerator 提供的方法支援適用於任何位址類型的標準連結產生功能。 使用連結產生器的最便利方式是透過執行特定位址類型作業的擴充方法:

擴充方法 描述
GetPathByAddress 根據提供的值產生具有絕對路徑的 URI。
GetUriByAddress 根據提供的值產生絕對 URI。

警告

注意呼叫 LinkGenerator 方法的下列影響:

  • 使用 GetUri* 擴充方法,並注意應用程式組態不會驗證傳入要求的 Host 標頭。 如果傳入要求的 Host 標頭未經驗證,則可能將未受信任的要求輸入傳回檢視或頁面 URI 中的用戶端。 建議所有生產應用程式將其伺服器設定為驗證 Host 標頭是否為已知有效值。

  • 使用 LinkGenerator,並注意與 MapMapWhen 搭配使用的中介軟體。 Map* 會變更執行要求的基底路徑,這會影響連結產生的輸出。 所有的 LinkGenerator API 都允許指定基底路徑。 指定空基底路徑,以復原 Map* 對連結產生的影響。

中介軟體範例

在下列範例中,中介軟體會使用 LinkGenerator API 來建立列出市集產品的動作方法連結。 應用程式中的任何類別都可以使用連結產生器,方法是將它插入類別並呼叫 GenerateLink

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

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

路由範本

{} 內的權杖定義路由參數,這些參數會在路由相符時繫結。 路由區段中可以定義多個路由參數,但路由參數必須以常值分隔。 例如:

{controller=Home}{action=Index}

不是有效的路由,因為 {controller}{action} 之間沒有常值。 路由參數必須具有名稱,而且可能已指定其他屬性。

路由參數之外的常值文字 (例如,{id}) 和路徑分隔符號 / 必須符合 URL 中的文字。 文字比對不區分大小寫,並根據 URL 路徑的解碼標記法。 若要比對常值路由參數分隔符號 {},請重複字元來逸出分隔符號。 例如 {{}}

星號 * 或雙星號 **

  • 可作為路由參數的前置詞,以繫結至 URI 的其餘部分。
  • 稱為 catch-all 參數。 例如 blog/{**slug}
    • 比對開頭為 blog/ 的任何 URI,並在它之後具有任何值。
    • 下列 blog/ 值會指派給路由值 slug

警告

由於路由中的錯誤 (bug)catch-all 參數可能會錯誤比對路由。 受到此錯誤 (bug) 影響的應用程式具有下列特性:

  • catch-all 路由,例如 {**slug}"
  • catch-all 路由無法比對應該相符的要求。
  • 移除其他路由讓 catch-all 路由開始運作。

如需發生此錯誤 (bug) 的範例案例,請參閱 GitHub 錯誤 (bug) 1867716579

這個錯誤 (bug) 的加入修正包含在 .NET Core 3.1.301 SDK 和更新版本。 下列程式碼會設定修正此錯誤 (bug) 的內部參數:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

全部擷取參數也可以符合空字串。

當使用路由產生 URL (包括路徑分隔符號 / 字元) 時,catch-all 參數會逸出適當的字元。 例如,路由值為 { path = "my/path" } 的路由 foo/{*path} 會產生 foo/my%2Fpath。 請注意逸出的斜線。 若要反覆存取路徑分隔符號字元,請使用 ** 路由參數前置詞。 具有 { path = "my/path" } 的路由 foo/{**path} 會產生 foo/my/path

URL 模式嘗試擷取具有選擇性副檔名的檔案名稱時,具有其他考量。 以範本 files/{filename}.{ext?} 為例。 當 filenameext 都存在值時,就會填入這兩個值。 如果 URL 中只有 filename 的值存在,路由會比對,因為尾端 . 是選擇性的。 下列 URL 符合此路由:

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

路由參數可能有「預設值」,指定方法是在參數名稱之後指定預設值,並以等號 (=) 分隔。 例如,{controller=Home} 定義 Home 作為 controller 的預設值。 如果 URL 中沒有用於參數的任何值,則會使用預設值。 路由參數也可以設為選擇性,方法是在參數名稱結尾附加問號 (?)。 例如: id? 。 選擇性值與預設路由參數之間的差異如下:

  • 具有預設值的路由參數一律會產生值。
  • 選擇性參數只有在要求 URL 提供值時才具有值。

路由參數可能具有條件約束,這些條件約束必須符合與 URL 繫結的路由值。 在路由參數名稱後面新增 : 和條件約束名稱,以指定路由參數上的內嵌條件約束。 如果條件約束需要引數,這些引數會在條件約束名稱後面以括弧 (...) 括住。 您可以附加另一個 : 和條件約束名稱,以指定多個內嵌條件約束

條件約束名稱和引述會傳遞至 IInlineConstraintResolver 服務來建立 IRouteConstraint 的執行個體,以用於 URL 處理。 例如,路由範本 blog/{article:minlength(10)} 指定具有引數 10minlength 條件約束。 如需路由條件約束和架構所提供之條件約束清單的詳細資訊,請參閱路由條件約束一節。

路由參數也可能有參數轉換器。 參數轉換器會在產生連結和比對動作和頁面至 URL 時,轉換參數的值。 如同條件約束,參數轉換器可以藉由在路由參數名稱後面新增 : 和轉換器名稱,以內嵌方式新增至路由參數。 例如,路由範本 blog/{article:slugify} 會指定 slugify 轉換器。 如需參數轉換器的詳細資訊,請參閱參數轉換器一節。

下表示範範例路由範本及其行為:

路由範本 範例比對 URI 要求 URI
hello /hello 只比對單一路徑 /hello
{Page=Home} / 比對並將 Page 設定為 Home
{Page=Home} /Contact 比對並將 Page 設定為 Contact
{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
{controller=Home}/{action=Index}/{id?} /Products 對應至 Products 控制器和 Index 方法。 已忽略 id

使用範本通常是最簡單的路由方式。 條件約束和預設值也可以在路由範本外部指定。

複雜區段

複雜區段會以非窮盡方式,比對從右至左的常值分隔符號來處理。 例如,[Route("/a{b}c{d}")] 是複雜的區段。 複雜區段會以特定方式運作,必須了解才能成功使用這些區段。 本節中的範例示範當分隔符號文字未出現在參數值內時,複雜區段才真正正常運作的原因。 使用 RegEx,然後手動擷取更複雜的案例需要值。

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

這是路由使用範本執行的步驟摘要,/a{b}c{d} 和 URL 路徑 /abcd| 可用來協助視覺化演算法的運作方式:

  • 第一個常值,由右至左,是 c。 因此,/abcd 會從右搜尋,並尋找 /ab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/ab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 沒有剩餘的文字,也沒有剩餘的路由範本,所以這是相符的。

以下是使用相同範本 /a{b}c{d} 和 URL 路徑 /aabcd 的負案例範例。 | 可用來協助視覺化演算法的運作方式。 此案例與相同演算法所說明的不相符:

  • 第一個常值,由右至左,是 c。 因此,/aabcd 會從右搜尋,並尋找 /aab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/aab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /a|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 此時有剩餘的文字 a,但演算法已用盡路由範本來剖析,因此這不是相符項目。

由於比對演算法非窮盡

  • 它會比對每個步驟中可能最少的文字量。
  • 在參數值內出現分隔符號值的任何情況,都會導致不相符。

正則運算式可更充分地控制其比對行為。

窮盡比對,也稱為延遲比對,符合最大的可能字串。 非窮盡符合最小的可能字串。

具有特殊字元的路由

使用特殊字元的路由可能會導致非預期的結果。 例如,請考慮使用下列動作方法的控制器:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id 包含下列編碼值時,可能會發生非預期的結果:

ASCII 已編碼
/ %2F
+

路由參數不一定會解碼 URL。 未來可能會解決此問題。 如需詳細資訊,請參閱這個 GitHub 問題

路由條件約束

路由條件約束執行時機是出現符合傳入 URL 的項目,並將 URL 路徑語彙基元化成路由值時。 路由條件約束通常會透過路由範本檢查相關聯的路由值,並針對值是否可接受做出 [True] 或 [False] 的決策。 某些路由條件約束會使用路由值以外的資料,以考慮是否可以路由要求。 例如,HttpMethodRouteConstraint 可以依據其 HTTP 指令動詞接受或拒絕要求。 條件約束可用於路由要求和連結產生。

警告

請勿針對輸入驗證使用條件約束。 如果條件約束用於輸入驗證,則不正確輸入會導致 404 找不到回應。 不正確輸入應該會產生 400 錯誤要求,並顯示適當的錯誤訊息。 路由條件約束會用來釐清類似的路由,而不是用來驗證特定路由的輸入。

下表示範範例路由條件約束及其預期行為:

條件約束 範例 範例相符項目 備註
int {id:int} 123456789, -123456789 符合任何整數
bool {active:bool} trueFALSE 比對 truefalse。 不區分大小寫
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm 比對不因文化特性而異中的有效 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 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

以冒號分隔的多個條件約束,可以套用至單一參數。 例如,下列條件約束會將參數限制在 1 或更大的整數值:

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

警告

驗證 URL 並轉換成 CLR 類型的路由條件約束一律使用不因文化特性而異。 例如,轉換成 CLR 類型 intDateTime。 這些條件約束假設 URL 無法當地語系化。 架構提供的路由條件約束不會修改路由值中儲存的值。 所有從 URL 剖析而來的路由值會儲存為字串。 例如,float 條件約束會嘗試將路由值轉換成浮點數,但轉換的值只能用來確認它可以轉換成浮點數。

條件約束中的規則運算式

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

規則運算式可以使用 regex(...) 路由條件約束來指定為內嵌條件約束。 MapControllerRoute 系列中的方法也會接受條件約束的物件常值。 如果使用該表單,字串值會解譯為規則運算式。

下列程式碼使用內嵌 RegEx 條件約束:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

下列程式碼會使用物件常值來指定 RegEx 條件約束:

app.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}$,請使用下列其中一項:

  • 將字串中提供的 \ 字元取代為 C# 來源檔案中的 \\ 字元,以逸出 \ 字串逸出字元。
  • 逐字字串常值

若要逸出路由參數分隔符號 {}[]、運算式中的字元,例如,{{}}[[]]。 下表顯示規則運算式及其逸出的版本:

規則運算式 逸出的規則運算式
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

路由中使用的規則運算式通常會以 ^ 字元開頭,並符合字串的起始位置。 運算式通常會以 $ 字元結尾,並符合字串結尾。 ^$ 字元可確保規則運算式符合整個路由參數值。 如果沒有 ^$ 個字元,規則運算式會比對字串內的任何子字串,這通常是不想要的。 下表提供範例,並說明它們符合或無法符合的原因:

運算式 String 比對 註解
[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 規則運算式

若要將參數限制為一組已知的可能值,請使用規則運算式。 例如,{action:regex(^(list|get|create)$)} 只會將 action 路由值與 listgetcreate 相符。 如果已傳入條件約束字典,字串 ^(list|get|create)$ 則是對等項目。 在不符合其中一個已知條件約束的條件約束字典中傳遞的條件約束也會被視為規則運算式。 在不符合其中一個已知條件約束的範本內傳遞的條件約束不會被視為規則運算式。

自訂路由條件約束

您可以實作 IRouteConstraint 介面來建立自訂路由條件約束。 IRouteConstraint 介面包含 Match,如果滿足條件約束,則會傳回 true,否則會傳回 false

自訂路由條件約束很少需要。 實作自訂路由條件約束之前,請考慮替代專案,例如模型繫結。

ASP.NET Core Constraints 資料夾提供建立條件約束的良好範例。 例如,GuidRouteConstraint

若要使用自訂 IRouteConstraint,路由條件約束類型必須向服務容器中的應用程式 ConstraintMap 註冊。 ConstraintMap 是一個目錄,它將路由限制式機碼對應到可驗證那些限制式的 IRouteConstraint 實作。 應用程式 ConstraintMap 可以在 Program.cs 中更新為 AddRouting 呼叫的一部分,或是直接使用 builder.Services.Configure<RouteOptions> 設定 RouteOptions。 例如:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上述條件約束會套用在下列程式碼中:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint 的實作可防止路由參數中使用 0

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[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 var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

上述 程式碼:

  • 防止路由 {id} 區段中的 0
  • 示範如何提供實作自訂條件約束的基本範例。 它不應該用於生產應用程式。

以下程式碼是防止處理包含 0id 的較佳方法:

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

    return Content(id);
}

上述程式碼在 NoZeroesRouteConstraint 方法上具有下列優點:

  • 它不需要自訂條件約束。
  • 當路由參數包含 0 時,它會傳回更具描述性的錯誤。

參數轉換器

參數轉換程式:

例如,具有 Url.Action(new { article = "MyTestArticle" }) 之路由模式 blog\{article:slugify} 中的自訂 slugify 參數轉換器,會產生 blog\my-test-article

請考慮下列 IOutboundParameterTransformer 實作:

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

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

若要在路由模式中使用參數轉換器,請使用 Program.cs 中的 ConstraintMap 進行設定:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core 架構會使用參數轉換器來轉換端點解析的 URI。 例如,參數轉換器會轉換用來比對 areacontrolleractionpage 的路由值:

app.MapControllerRoute(
    name: "default",
    pattern: "{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 頁面。 如需詳細資訊,請參閱控制器中的路由

URL 產生流程會從呼叫 LinkGenerator.GetPathByAddress 或類似的方法開始。 方法會提供位址、一組路由值,以及選擇性地從 HttpContext 取得目前要求的相關資訊。

第一個步驟是使用位址來解析一組候選端點,其 IEndpointAddressScheme<TAddress> 符合位址的類型。

一旦位址配置找到一組候選項目之後,端點會逐一排序並反復處理,直到 URL 產生作業成功為止。 URL 產生不會檢查模棱兩可,傳回的第一個結果會是最終結果。

使用記錄對 URL 產生進行疑難排解

疑難排解 URL 產生的第一個步驟是將 Microsoft.AspNetCore.Routing 的記錄層級設定為 TRACELinkGenerator 記錄其處理的許多詳細資料,這對於疑難排解問題很有用。

如需 URL 產生的詳細資訊,請參閱 URL 產生參考

地址

位址是 URL 產生中用來將呼叫繫結至連結產生器至一組候選端點的概念。

位址是預設隨附兩個實作的可延伸概念:

  • 使用端點名稱 (string) 作為位址:
    • 提供與 MVC 路由名稱類似的功能。
    • 使用 IEndpointNameMetadata 中繼資料類型。
    • 針對所有已註冊端點的中繼資料解析提供的字串。
    • 如果多個端點使用相同的名稱,則會在啟動時擲回例外狀況。
    • 建議在控制器和 Razor 頁面外部使用一般用途。
  • 使用路由值 (RouteValuesAddress) 作為位址:
    • 提供與控制器和 Razor 頁面舊版 URL 產生類似的功能。
    • 擴充和偵錯非常複雜。
    • 提供 IUrlHelper、標籤協助程式、HTML 協助程式、動作結果等所使用的實作。

位址配置的角色是依任意準則在位址與相符端點之間建立關聯:

  • 端點名稱配置會執行基本字典查閱。
  • 路由值配置具有複雜的集合演算法子集。

環境值和明確值

從目前的要求,路由會存取目前要求的路由值 HttpContext.Request.RouteValues。 與目前要求相關聯的值稱為環境值。 為了清楚起見,文件將傳入方法的路由值稱為明確值

下列範例顯示環境值和明確值。 它提供目前要求的環境值和明確值:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上述 程式碼:

下列程式碼只提供明確值,而且沒有環境值:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

上述方法會傳回 /Home/Subscribe/17

WidgetController 中的下列程式碼會傳回 /Widget/Subscribe/17

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

下列程式碼會從目前要求中的環境值和明確值提供控制器:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

在上述程式碼中:

  • 傳回 /Gadget/Edit/17
  • Url 取得 IUrlHelper
  • Action 會產生具有動作方法絕對路徑的 URL。 URL 包含指定的 action 名稱和 route 值。

下列程式碼提供來自目前要求的環境值和明確值:

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

        // ...
    }
}

上述程式碼會將編輯 Razor 頁面包含下列頁面指示詞時,將 url 設定為 /Edit/17

@page "{id:int}"

如果 [編輯] 頁面不包含 "{id:int}" 路由範本,url 則為 /Edit?id=17

MVC IUrlHelper 的行為除了此處所述的規則之外,也會新增一層複雜度:

  • IUrlHelper 一律提供目前要求的路由值作為環境值。
  • 除非開發人員覆寫,否則 IUrlHelper.Action 一律會將目前 action 值和 controller 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會將目前的 page 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會覆寫目前 handler 路由值,其中 null 做為明確值。

使用者通常對環境值的行為詳細資料感到驚訝,因為 MVC 似乎沒有遵循自己的規則。 基於歷程記錄和相容性考慮,某些路由值,例如 actioncontrollerpagehandler 有自己的特殊案例行為。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage 所提供的對等功能會複製這些 IUrlHelper 的異常,以取得相容性。

URL 產生流程

找到一組候選端點之後,URL 產生演算法:

  • 反復處理端點。
  • 傳回第一個成功的結果。

此程式中的第一個步驟稱為路由值無效。 路由值失效是路由決定應該使用環境值和應忽略哪些路由值的流程。 系統會考慮每個環境值,並結合明確值或忽略。

思考環境值角色的最佳方式是,他們嘗試在一些常見情況下儲存應用程式開發人員輸入。 傳統上,環境值有用的案例與 MVC 相關:

  • 連結至相同控制器中的另一個動作時,不需要指定控制器名稱。
  • 連結至相同區域中的另一個控制器時,不需要指定區功能變數名稱。
  • 連結至相同的動作方法時,不需要指定路由值。
  • 連結至應用程式的另一個部分時,您不想傳遞在應用程式該部分沒有意義的路由值。

對傳回 nullLinkGeneratorIUrlHelper 的呼叫通常是因為無法了解路由值無效所造成。 藉由明確指定更多路由值來針對路由值失效進行疑難排解,以查看這是否可解決問題。

路由值失效的運作方式是假設應用程式的 URL 配置是階層式,而階層是由左至右形成。 請考慮基本控制器路由範本 {controller}/{action}/{id?},以直覺方式了解實際運作方式。 值變更會使所有出現在右邊的路由值失效。 這反映了階層的假設。 如果應用程式有 id 的環境值,則作業會為 controller 指定不同的值:

  • id 不會重複使用,因為 {controller} 位於 {id?} 的左邊。

示範此原則的一些範例:

  • 如果明確值包含 id 的值,則會忽略 id 的環境值。 可以使用 controlleraction 的環境值。
  • 如果明確值包含 action 的值,則會忽略 action 的任何環境值。 可以使用 controller 的環境值。 如果 action 的明確值與 action 的環境值不同,則不會使用 id 值。 如果 action 的明確值與 action 的環境值相同,則可以使用 id 值。
  • 如果明確值包含 controller 的值,則會忽略 controller 的任何環境值。 如果 controller 的明確值與 controller 的環境值不同,則不會使用 actionid 值。 如果 controller 的明確值與 controller 的環境值相同,則可以使用 actionid 值。

透過屬性路由和專用傳統路由的存在,此流程會進一步複雜。 控制器傳統路由,例如 {controller}/{action}/{id?} 使用路由參數指定階層。 針對專用傳統路由屬性路由控制器和 Razor 頁面:

  • 路由值有階層。
  • 它們不會出現在範本中。

在這些情況下,URL 產生會定義必要值概念。 控制器和 Razor 頁面所建立的端點具有指定的必要值,可讓路由值失效運作。

路由值無效演算法詳細資料:

  • 必要的值名稱會與路由參數結合,然後從左至右進行處理。
  • 針對每個參數,會比較環境值和明確值:
    • 如果環境值和明確值相同,流程會繼續。
    • 如果環境值存在且明確值不存在,則產生 URL 時會使用環境值。
    • 如果環境值不存在且明確值存在,請拒絕環境值和所有後續的環境值。
    • 如果環境值和明確值存在,且兩個值不同,請拒絕環境值和所有後續的環境值。

此時,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

選擇性路由參數順序

選擇性路由參數必須位在所有必要的路由參數之後。 在下列程式碼中, idname 參數必須位於 color 參數之後:

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}

路由值不正確問題

下列程式碼顯示路由不支援的 URL 產生配置範例:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

在上述程式碼中,會使用 culture 路由參數進行當地語系化。 想要讓 culture 參數一律接受為環境值。 不過,由於所需值的運作方式,culture 參數不會接受為環境值:

  • "default" 路由範本中,culture 路由參數位於 controller 的左邊,因此 controller 的變更不會使 culture無效。
  • "blog" 路由範本中,culture 路由參數會視為 controller 的右邊,其會出現在必要值中。

使用 LinkParser 剖析 URL 路徑

LinkParser 類別會將剖析 URL 路徑的支援新增至一組路由值。 ParsePathByEndpointName 方法會採用端點名稱和 URL 路徑,並傳回從 URL 路徑擷取的一組路由值。

在下列範例控制器中,GetProduct 動作會使用 api/Products/{id} 的路由範本,並具有 GetProductName

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

在相同的控制器類別中,AddRelatedProduct 動作需要 URL 路徑,pathToRelatedProduct,其可作為查詢字串參數提供:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

在上述範例中,AddRelatedProduct 動作會從 URL 路徑擷取 id 路由值。 例如,使用 /api/Products/1 的 URL 路徑,relatedProductId 值會設定為 1。 此方法可讓 API 的用戶端在參考資源時使用 URL 路徑,而不需要了解這類 URL 的結構。

設定端點中繼資料

下列連結提供如何設定端點中繼資料的資訊:

搭配 RequireHost 路由中的主機比對

RequireHost 會將條件約束套用至需要指定主機的路由。 RequireHost 或 [Host] 參數可以是:

  • 主機:www.domain.com,符合任何連接埠 www.domain.com
  • 具有萬用字元的主機:*.domain.com、比對任何連接埠上的 www.domain.comsubdomain.domain.comwww.subdomain.domain.com
  • 連接埠:*:5000,符合連接埠 5000 與任何主機。
  • 主機和連接埠:www.domain.com:5000*.domain.com:5000,符合主機和連接埠。

可以使用 RequireHost[Host] 來指定多個參數。 條件約束會比對任何參數有效的主機。 例如,[Host("domain.com", "*.domain.com")] 符合 domain.comwww.domain.comsubdomain.domain.com

下列程式碼會使用 RequireHost 在路由上要求指定的主機:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

下列程式碼會使用控制器上的 [Host] 屬性來要求任何指定的主機:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 屬性同時套用至控制器和動作方法時:

  • 會使用動作上的屬性。
  • 控制器屬性會被忽略。

路由群組

MapGroup 擴充方法可協助組織具有常見前置詞的端點群組。 其可減少重複的程式碼,並允許使用單一呼叫方法 (例如 RequireAuthorizationWithMetadata,其可新增端點中繼資料) 來自訂整個端點群組。

例如,下列程式碼會建立兩個類似的端點群組:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

在此案例中,您可以在 201 Created 結果中使用 Location 標頭的相對位址:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

第一組端點只會比對以 /public/todos 為前置詞且不需要任何驗證即可存取的要求。 第二組端點只會比對以 /private/todos 為前置詞且需要驗證的要求。

QueryPrivateTodos端點篩選處理站是本機函式,可修改路由處理常式的 TodoDb 參數,以允許存取及儲存私人 Todo 資料。

路由群組也支援具有路由參數和條件約束的巢狀群組和複雜前置詞模式。 在下列範例中,對應至 user 群組的路由處理常式可以擷取外部群組前置詞中定義的 {org}{group} 路由參數。

前置詞也可能是空的。 這適用於將端點中繼資料或篩選條件新增至一組端點,而不需要變更路由模式。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

將篩選條件或中繼資料新增至群組的行為,與將篩選條件或中繼資料個別新增至每個端點,再新增任何可能已新增至內部群組或特定端點的額外篩選條件或中繼資料的方式相同。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

在上述範例中,即使已新增第二個要求,外部篩選條件也會在內部篩選條件之前記錄傳入要求。 由於篩選條件已套用至不同的群組,因此彼此相對的新增順序並不重要。 如果套用至相同的群組或特定端點,則篩選條件的新增順序就很重要。

/outer/inner/ 的要求會記錄下列內容:

/outer group filter
/inner group filter
MapGet filter

路由的效能指引

當應用程式發生效能問題時,通常會懷疑路由有問題。 路由的原因是控制器和 Razor 頁面等架構會報告架構在其記錄訊息中花費的時間量。 當控制器報告的時間與要求的總時間之間有顯著差異時:

  • 開發人員會將其應用程式程式碼排除為問題的來源。
  • 通常假設路由是原因。

路由是使用數千個端點測試的效能。 一般應用程式不太可能因為太大而遇到效能問題。 路由效能緩慢最常見的根本原因是自訂中介軟體行為不良。

下列程式碼範例示範縮小延遲來源的基本技巧:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

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

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

app.UseRouting();

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

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

app.UseAuthorization();

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

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

app.MapGet("/", () => "Timing Test.");

計時路由:

  • 將每個中介軟體與上述程式碼中顯示的計時中介軟體複本交錯。
  • 新增唯一識別碼,以將計時資料與程式碼相互關聯。

這是一種基本方式,可以在延遲顯著時縮小延遲,例如,超過 10ms。 從 Time 1 減去 Time 2 會報告 UseRouting 中介軟體內所花費的時間。

下列程式碼會使用更精簡的方法來處理上述計時程式碼:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

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

app.UseRouting();

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

app.UseAuthorization();

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

app.MapGet("/", () => "Timing Test.");

潛在昂貴的路由功能

下列清單提供相較於基本路由範本相對昂貴之路由功能的一些見解:

  • 規則運算式:可以撰寫複雜的規則運算式,或有少量輸入的長時間執行時間。
  • 複雜區段 ({x}-{y}-{z}):
    • 比剖析一般 URL 路徑區段更昂貴。
    • 導致配置更多子字串。
  • 同步資料存取:許多複雜的應用程式在其路由中具有資料庫存取權。 使用擴充點,例如 MatcherPolicyEndpointSelectorContext,這是非同步。

大型路由表的指引

根據預設,ASP.NET Core 會使用路由演算法來交換 CPU 時間的記憶體。 這有一個良好的效果,路由比對時間只取決於路徑的長度,而不是路由數目。 不過,在某些情況下,當應用程式有大量的路由 (數千個),而且路由中有大量的變數前置詞時,這種方法可能會有問題。 例如,如果路由在路由的早期區段中具有參數,例如 {parameter}/some/literal

除非下列情況,否則應用程式不太可能遇到此問題的情況:

  • 此模式在應用程式中有許多路由。
  • 應用程式中有許多路由。

如何判斷應用程式是否正在發生大型路由表問題

  • 有兩個徵兆需要尋找:
    • 應用程式在第一個要求上啟動的速度很慢。
      • 請注意,這是必要項目,但並不足夠。 還有其他許多非路由問題,可能會導致應用程式啟動緩慢。 檢查下列條件,以正確判斷應用程式是否正在發生這種情況。
    • 應用程式會在啟動期間耗用大量的記憶體,而記憶體傾印會顯示大量 Microsoft.AspNetCore.Routing.Matching.DfaNode 執行個體。

如何解決此問題

有數種技術和最佳化可以套用至大幅改善此案例的路由:

  • 視需要將路由條件約束套用至參數,例如 {parameter:int}{parameter:guid}{parameter:regex(\\d+)} 等。
    • 這可讓路由演算法在內部最佳化用於比對的結構,並大幅減少所使用的記憶體。
    • 在絕大多數情況下,這足以回到可接受的行為。
  • 變更路由以將參數移至範本中的稍後區段。
    • 這樣可減少可能「路徑」的數目,以符合指定路徑的端點。
  • 使用動態路由,並動態執行控制器/頁面的對應。
    • 這可以使用 MapDynamicControllerRouteMapDynamicPageRoute 來達成。

程式庫作者的指引

本節包含程式庫作者建置在路由之上的指引。 這些詳細資料旨在確保應用程式開發人員有良好的使用程式庫和架構來擴充路由的體驗。

定義端點

若要建立使用路由進行 URL 比對的架構,請先定義建置在 UseEndpoints 之上的使用者體驗。

DO 建置在 IEndpointRouteBuilder 之上。 這可讓使用者與其他 ASP.NET Core 功能撰寫架構,而不會造成混淆。 每個 ASP.NET Core 範本都包含路由。 假設使用者有路由存在且熟悉。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DO 會從實作 IEndpointConventionBuilderMapMyFramework(...) 呼叫傳回密封具體型別。 大部分的架構 Map... 方法都遵循此模式。 IEndpointConventionBuilder 介面:

  • 允許撰寫中繼資料。
  • 以各種擴充方法為目標。

宣告您自己的型別可讓您將自己的架構特定功能新增至建立器。 可以包裝架構宣告的建立器,並將呼叫轉接給它。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

考慮撰寫您自己的 EndpointDataSourceEndpointDataSource 是宣告和更新端點集合的低層級基本類型。 EndpointDataSource 是控制器和 Razor 頁面所使用的強大 API。

路由測試具有非更新資料來源基本範例

考慮實作 GetGroupedEndpoints。 這可完整控制執行群組慣例,以及群組端點上的最終中繼資料。 例如,這可讓自訂 EndpointDataSource 實作執行端點篩選新增至群組。

根據預設,請勿嘗試註冊 EndpointDataSource。 要求使用者在 UseEndpoints 中註冊您的架構。 路由的理念是預設不會包含任何項目,而且 UseEndpoints 是註冊端點的位置。

建立路由整合式中介軟體

考慮將中繼資料型別定義為介面。

DO 可讓您在類別和方法上使用中繼資料型別作為屬性。

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

控制器和 Razor 頁面等架構支援將中繼資料屬性套用至型別和方法。 如果您宣告中繼資料型別:

  • 將它們設為可存取的屬性
  • 大部分的使用者都熟悉套用屬性。

將中繼資料型別宣告為介面會新增另一層彈性:

  • 介面是可組合的。
  • 開發人員可以宣告自己結合多個原則的型別。

DO 可讓您覆寫中繼資料,如下列範例所示:

[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() { }
}

遵循這些指導方針的最佳方式是避免定義標記中繼資料

  • 不要只尋找中繼資料型別是否存在。
  • 在中繼資料上定義屬性,並檢查屬性。

中繼資料集合是排序的,並支援依優先順序覆寫。 在控制器的情況下,動作方法上的中繼資料最具體。

DO 讓中介軟體在不使用路由的情況下使用:

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

// Your framework
app.MapMyFramework(...).RequireAuthorization();

作為此指導方針的範例,請考慮 UseAuthorization 中介軟體。 授權中介軟體可讓您傳入後援原則。 如果指定後援原則,則適用於兩者:

  • 不含指定原則的端點。
  • 不符合端點的要求。

這可讓授權中介軟體在路由內容之外很有用。 授權中介軟體可用於傳統中介軟體程式設計。

偵錯診斷

如需詳細的路由診斷輸出,請將 Logging:LogLevel:Microsoft 設定為 Debug。 在開發環境中,在 appsettings.Development.json 中設定記錄層級:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

其他資源

路由負責比對連入 HTTP 要求,並將這些要求分派至應用程式的可執行端點。 端點是應用程式的可執行檔要求處理程式碼單位。 端點會在應用程式中定義,並在應用程式啟動時設定。 端點比對程式可以從要求的 URL 擷取值,並提供這些值以供要求處理。 使用來自應用程式的端點資訊,路由也能夠產生對應至端點的 URL。

應用程式可以使用下列方式設定路由:

本文涵蓋路由 ASP.NET Core 低層級詳細資料。 如需設定路由的資訊:

路由的基本概念

下列程式碼顯示路由的基本範例:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

上述範例包含使用 MapGet 方法的單一端點:

  • 當 HTTP GET 要求傳送至根 URL / 時:
    • 要求委派會執行。
    • Hello World! 會寫入 HTTP 回應。
  • 如果要求方法不是 GET 或根 URL 不是 /,則不會比對路由,而且會傳回 HTTP 404。

路由使用一對中介軟體,由 UseRoutingUseEndpoints 註冊:

  • UseRouting 會將路由比對新增至中介軟體管線。 此中介軟體會查看應用程式中定義的端點集,並根據要求選取最佳端點比對
  • UseEndpoints 將端點執行新增至中介軟體管線。 它會執行與所選端點相關聯的委派。

應用程式通常不需要呼叫 UseRoutingUseEndpointsWebApplicationBuilder 設定中介軟體管線,此管線會包裝使用 UseRoutingUseEndpointsProgram.cs 中新增的中介軟體。 不過,應用程式可以藉由明確呼叫這些方法,變更 UseRoutingUseEndpoints 執行的順序。 例如,下列程式碼會明確呼叫 UseRouting

app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");

在上述程式碼中:

  • app.Use 的呼叫會註冊在管線開頭執行的自訂中介軟體。
  • 呼叫 UseRouting 會設定路由比對中介軟體,以在自訂中介軟體之後執行。
  • MapGet 註冊的端點會在管線結尾執行。

如果上述範例未包含對 UseRouting 的呼叫,則自訂中介軟體會在路由比對中介軟體之後執行。

端點

MapGet 方法可用來定義端點。 端點可以是:

  • 選取,方法是比對 URL 和 HTTP 方法。
  • 執行,透過執行委派。

應用程式可以比對和執行的端點會在 UseEndpoints 中設定。 例如,MapGetMapPost類似的方法,將要求委派連接到路由系統。 其他方法可用來將 ASP.NET Core 架構功能連線至路由系統:

下列範例示範使用更複雜的路由範本進行路由:

app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");

字串 /hello/{name:alpha}路由範本。 路由範本可用來設定端點的比對方式。 在此情況下,範本會比對:

  • URL,例如 /hello/Docs
  • 開頭為 /hello/ 後面接著一連串字母字元的任何 URL 路徑。 :alpha 套用只符合字母字元的路由條件約束。 本文稍後會說明路由條件約束

URL 路徑的第二個區段 {name:alpha}

下列範例顯示具有健康狀態檢查和授權的路由:

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");

上述範例示範如何:

  • 授權中介軟體可以搭配路由使用。
  • 端點可用來設定授權行為。

MapHealthChecks 呼叫會新增健康情況檢查端點。 鏈結 RequireAuthorization 至此呼叫會將授權原則附加至端點。

呼叫 UseAuthenticationUseAuthorization 以新增驗證和授權中介軟體。 這些中介軟體會放在 UseRoutingUseEndpoints 之間,讓它們能夠:

  • 查看 UseRouting 所選取的端點。
  • UseEndpoints 分派至端點之前,請先套用授權原則。

端點中繼資料

在上述範例中,有兩個端點,但只有健康情況檢查端點已附加授權原則。 如果要求符合健康情況檢查端點 /healthz,則會執行授權檢查。 這示範端點可以附加額外的資料。 此額外資料稱為端點中繼資料

  • 中繼資料可以透過路由感知中介軟體來處理。
  • 中繼資料可以是任何 .NET 類型。

路由概念

路由系統會藉由新增功能強大的端點概念,以建置在中介軟體管線之上。 端點代表應用程式功能單位,這些單位在路由、授權和任何數目 ASP.NET Core 的系統方面彼此不同。

ASP.NET Core 端點定義

ASP.NET Core 端點為:

下列程式碼示範如何擷取和檢查符合目前要求的端點:

app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");

如果選取端點,可以從 HttpContext 擷取。 可以檢查其屬性。 端點物件是不可變的,在建立之後無法修改。 最常見的端點類型是 RouteEndpointRouteEndpoint 包含允許路由系統選取的資訊。

在上述程式碼中,app.Use 設定內嵌中介軟體

下列程式碼顯示,視管線中呼叫 app.Use 的位置而定,可能沒有端點:

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

app.UseRouting();

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

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

app.UseEndpoints(_ => { });

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

上述範例會新增 Console.WriteLine 語句,以顯示是否已選取端點。 為了清楚起見,此範例會將顯示名稱指派給提供的 / 端點。

上述範例也包含呼叫 UseRoutingUseEndpoints,以完全控制這些中介軟體在管線內執行時間。

執行具有 / URL 的程式碼會顯示:

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

使用任何其他 URL 執行此程式碼會顯示:

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

此輸出示範:

  • 呼叫 UseRouting 之前,端點一律為 null。
  • 如果找到相符專案,則端點在 UseRoutingUseEndpoints 之間為非 null。
  • 找到相符項目時,UseEndpoints 中介軟體是終端。 本文稍後會定義終端中介軟體
  • 只有在找不到相符項目時,才會執行 UseEndpoints 後的中介軟體。

UseRouting 中介軟體會使用 SetEndpoint 方法來將端點附加至目前的內容。 您可以使用自訂邏輯取代 UseRouting 中介軟體,但仍可取得使用端點的權益。 端點是低層級基本類型,例如中介軟體,不會與路由實作結合。 大部分的應用程式不需要以自訂邏輯取代 UseRouting

UseEndpoints 中介軟體的設計目的是與 UseRouting 中介軟體搭配使用。 執行端點的核心邏輯並不複雜。 使用 GetEndpoint 來擷取端點,然後叫用其 RequestDelegate 屬性。

下列程式碼示範中介軟體如何影響或回應路由:

app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }

上述範例示範兩個重要概念:

  • 中介軟體可以在 UseRouting 之前執行,以修改路由運作的資料。
  • 中介軟體可以在 UseRoutingUseEndpoints 之間執行,以在執行端點之前處理路由的結果。
    • UseRoutingUseEndpoints之間執行的中介軟體:
      • 通常會檢查中繼資料以了解端點。
      • 通常會依照 UseAuthorizationUseCors 來完成安全性決策。
    • 中介軟體和中繼資料的組合可讓您設定每個端點的原則。

上述程式碼示範支援每個端點原則的自訂中介軟體範例。 中介軟體會將敏感性資料存取權的稽核記錄寫入主控台。 中介軟體可以設定為使用 RequiresAuditAttribute 中繼資料端點稽核。 此範例示範加入宣告模式,其中只會稽核標示為敏感性的端點。 您可以反向定義此邏輯,例如稽核未標示為安全的所有內容。 端點中繼資料系統具有彈性。 此邏輯可以設計成任何適合使用案例的方式。

上述範例程式碼旨在示範端點的基本概念。 此範例不適用於生產環境稽核記錄中介軟體的更完整版本會:

  • 登入檔案或資料庫。
  • 包含詳細資料,例如使用者、IP 位址、敏感性端點的名稱等。

稽核原則中繼資料 RequiresAuditAttribute 定義為 Attribute,以便更輕鬆地與控制器和 SignalR 等類別架構搭配使用。 使用 路由至程式碼 時:

  • 中繼資料會與產生器 API 連結。
  • 以類別為基礎的架構會在建立端點時,包含對應方法和類別上的所有屬性。

中繼資料類型的最佳做法是將它們定義為介面或屬性。 介面和屬性允許重複使用程式碼。 中繼資料系統具有彈性,而且不會施加任何限制。

比較終端中介軟體與路由

下列範例示範終端中介軟體和路由:

// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");

Approach 1: 顯示的中介軟體樣式為終端中介軟體。 它稱為終端中介軟體,因為它會執行比對作業:

  • 上述範例中的比對作業針對中介軟體是 Path == "/",針對路由是 Path == "/Routing"
  • 比對成功時,它會執行一些功能並傳回,而不是叫用 next 中介軟體。

它稱為終端中介軟體,因為它會終止搜尋、執行某些功能,然後傳回。

下列清單會比較終端中介軟體與路由:

  • 這兩種方法都允許終止處理管線:
    • 中介軟體會傳回而不是叫用 next 來終止管線。
    • 端點一律為終端。
  • 終端中介軟體允許將中介軟體放置在管線中的任意位置:
  • 終端中介軟體可讓任意程式碼判斷中介軟體何時符合:
    • 自訂路由比對程式碼可能很詳細且難以正確撰寫。
    • 路由為一般應用程式提供直接的解決方案。 大部分的應用程式都不需要自訂路由比對程式碼。
  • 端點介面與中介軟體,例如 UseAuthorizationUseCors
    • 使用具有 UseAuthorizationUseCors 的終端中介軟體需要手動與授權系統互動。

端點定義兩者:

  • 處理要求的委派。
  • 任意中繼資料的集合。 中繼資料是用來根據附加至每個端點的原則和組態來實作跨領域考量。

終端中介軟體可以是有效的工具,但可能需要:

  • 大量的程式碼撰寫和測試。
  • 手動與其他系統整合,以達到所需的彈性層級。

在撰寫終端中介軟體之前,請考慮與路由整合。

MapMapWhen 整合的現有終端中介軟體通常可以轉換成路由感知端點。 MapHealthChecks 示範路由器軟體的模式:

下列程式碼示範如何使用 MapHealthChecks

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();

上述範例顯示為何傳回建立器物件很重要。 傳回建立器物件可讓應用程式開發人員設定原則,例如端點的授權。 在此範例中,健康情況檢查中介軟體與授權系統沒有直接整合。

中繼資料系統是針對使用終端中介軟體的擴充性建立者所遇到的問題而建立的。 每個中介軟體實作自己的與授權系統的整合是有問題的。

URL 比對

  • 這是路由比對傳入要求與端點的流程。
  • 以 URL 路徑和標頭中的資料為基礎。
  • 可以擴充以考慮要求中的任何資料。

當路由中介軟體執行時,它會設定 Endpoint,並將值路由至目前要求 HttpContext 上的要求功能

在路由中介軟體可以檢查端點並採取動作之後,中介軟體執行。 例如,授權中介軟體可以詢問授權原則的端點中繼資料集合。 執行要求處理管線中的所有中介軟體之後,會叫用所選端點的委派。

端點路由中的路由系統負責制定所有分派決策。 因為中介軟體會根據選取的端點套用原則,因此請務必:

  • 任何可能影響分派或安全性原則應用的決定,都會在路由系統內進行。

警告

為了回溯相容性,當執行 Controller 或 Razor頁面端點委派時,RouteContext.RouteData 的屬性會根據到目前為止所執行的要求處理,設定為適當的值。

RouteContext 類型將在未來的版本中標示為過時:

  • RouteData.Values 移轉至 HttpRequest.RouteValues
  • 移轉 RouteData.DataTokens 以從端點中繼資料擷取 IDataTokensMetadata

URL 比對會在一組可設定的階段中運作。 在每個階段中,輸出是一組相符項目。 下一個階段可以進一步縮小一組相符項目。 路由實作不保證相符端點的處理順序。 所有可能的相符項目都會一次處理。 URL 比對階段會依下列順序進行。 ASP.NET Core:

  1. 針對一組端點及其路由範本處理 URL 路徑,並收集所有相符項目。
  2. 採用上述清單,並移除套用路由條件約束失敗的相符項目。
  3. 採用上述清單,並移除失敗 MatcherPolicy 執行個體集的相符項目。
  4. 使用 EndpointSelector 從上述清單做出最終決定。

端點清單會根據下列項目排定優先順序:

每個階段都會處理所有相符的端點,直到到達 EndpointSelector 為止。 EndpointSelector 是最後階段。 它會從相符項目中選擇最高的優先順序端點作為最佳相符項目。 如果其他相符項目具有與最佳相符項目相同的優先順序,則會擲回模棱兩可的比對例外狀況。

路由優先順序是根據更具體路由範本來計算,其優先順序較高。 例如,請考慮範本 /hello/{message}

  • 兩者都符合 URL 路徑 /hello
  • /hello 更具體,因此優先順序較高。

一般而言,路由優先順序會針對實際使用的 URL 配置類型,選擇最相符的作業。 只有在需要時才使用 Order 以避免模棱兩可。

由於路由所提供的擴充性種類,路由系統無法事先計算模棱兩可的路由。 請考慮路由範本 /{message:alpha}/{message:int} 等範例:

  • alpha 條件約束只符合字母字元。
  • int 條件約束只符合數字。
  • 這些範本具有相同的路由優先順序,但兩者都沒有相符的單一 URL。
  • 如果路由系統在啟動時回報模棱兩可的錯誤,則會封鎖此有效的使用案例。

警告

UseEndpoints 內的作業順序不會影響路由的行為,但有一個例外狀況。 MapControllerRouteMapAreaRoute 會根據叫用的順序,自動將訂單值指派給其端點。 這會模擬控制器的長期行為,而沒有路由系統提供與舊版路由實作相同的保證。

ASP.NET Core 中的端點路由:

  • 沒有路由的概念。
  • 不提供訂購保證。 所有端點都會一次處理。

路由範本優先順序和端點選取順序

路由範本優先順序是一個系統,會根據路由範本的特定程度,為每個路由範本指派值。 路由範本優先順序:

  • 避免在常見情況下調整端點的順序。
  • 嘗試比對路由行為的常識預期。

例如,請考慮範本 /Products/List/Products/{id}。 對於 URL 路徑 /Products/List,可以合理地假設 /Products/List/Products/{id} 更相符。 這可運作,因為常值區段 /List 的優先順序會比參數區段 /{id} 更好。

優先順序運作方式的詳細資料會與如何定義路由範本:

  • 具有更多區段的範本會被視為更具體。
  • 具有常值文字的區段會比參數區段更具體。
  • 具有條件約束的參數區段會被視為比不具條件的參數區段更具體。
  • 複雜區段會被視為具有條件約束的參數區段。
  • Catch-all 參數是最不具體的。 如需 catch-all 路由的重要資訊,請參閱路由範本一節中的 catch-all

URL 產生概念

URL 產生:

  • 這是路由可以根據一組路由值建立 URL 路徑的流程。
  • 允許在端點與存取端點的 URL 之間進行邏輯分隔。

端點路由包含 LinkGenerator API。 LinkGenerator 是單一資料庫,可從 DI 取得。 LinkGenerator API 可以在執行要求的內容之外使用。 Mvc.IUrlHelper 和依賴 IUrlHelper 的情節,例如標籤協助程式、HTML 協助程式,以及動作結果,請在內部使用 LinkGenerator API 來提供連結產生功能。

連結產生器背後支援的概念為「位址」和「位址配置」。 位址配置可讓您判斷應考慮用於連結產生的端點。 例如,路由名稱和路由值情節,許多使用者都熟悉來自控制器,Razor 頁面會實作為位址配置。

連結產生器可以透過下列擴充方法連結至控制器和 Razor 頁面:

這些方法的多載會接受包含 HttpContext 的引數。 這些方法的功能相當於 Url.ActionUrl.Page,但提供額外的彈性和選項。

GetPath* 方法最類似 Url.ActionUrl.Page,因為它們會產生包含絕對路徑的 URI。 GetUri* 方法一律會產生包含配置和主機的絕對 URI。 接受 HttpContext 的方法會在執行要求的內容中產生 URI。 除非覆寫,否則會使用來自執行要求的環境路由值、URL 基底路徑、配置和主機。

呼叫 LinkGenerator 並指定一個位址。 執行下列兩個步驟來產生 URI:

  1. 將位址繫結至符合該位址的端點清單。
  2. 評估每個端點的 RoutePattern,直到找到符合所提供值的路由模式。 產生的輸出會與提供給連結產生器的其他 URI 組件合併並傳回。

LinkGenerator 提供的方法支援適用於任何位址類型的標準連結產生功能。 使用連結產生器的最便利方式是透過執行特定位址類型作業的擴充方法:

擴充方法 描述
GetPathByAddress 根據提供的值產生具有絕對路徑的 URI。
GetUriByAddress 根據提供的值產生絕對 URI。

警告

注意呼叫 LinkGenerator 方法的下列影響:

  • 使用 GetUri* 擴充方法,並注意應用程式組態不會驗證傳入要求的 Host 標頭。 如果傳入要求的 Host 標頭未經驗證,則可能將未受信任的要求輸入傳回檢視或頁面 URI 中的用戶端。 建議所有生產應用程式將其伺服器設定為驗證 Host 標頭是否為已知有效值。

  • 使用 LinkGenerator,並注意與 MapMapWhen 搭配使用的中介軟體。 Map* 會變更執行要求的基底路徑,這會影響連結產生的輸出。 所有的 LinkGenerator API 都允許指定基底路徑。 指定空基底路徑,以復原 Map* 對連結產生的影響。

中介軟體範例

在下列範例中,中介軟體會使用 LinkGenerator API 來建立列出市集產品的動作方法連結。 應用程式中的任何類別都可以使用連結產生器,方法是將它插入類別並呼叫 GenerateLink

public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

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

路由範本

{} 內的權杖定義路由參數,這些參數會在路由相符時繫結。 路由區段中可以定義多個路由參數,但路由參數必須以常值分隔。 例如:

{controller=Home}{action=Index}

不是有效的路由,因為 {controller}{action} 之間沒有常值。 路由參數必須具有名稱,而且可能已指定其他屬性。

路由參數之外的常值文字 (例如,{id}) 和路徑分隔符號 / 必須符合 URL 中的文字。 文字比對不區分大小寫,並根據 URL 路徑的解碼標記法。 若要比對常值路由參數分隔符號 {},請重複字元來逸出分隔符號。 例如 {{}}

星號 * 或雙星號 **

  • 可作為路由參數的前置詞,以繫結至 URI 的其餘部分。
  • 稱為 catch-all 參數。 例如 blog/{**slug}
    • 比對開頭為 blog/ 的任何 URI,並在它之後具有任何值。
    • 下列 blog/ 值會指派給路由值 slug

警告

由於路由中的錯誤 (bug)catch-all 參數可能會錯誤比對路由。 受到此錯誤 (bug) 影響的應用程式具有下列特性:

  • catch-all 路由,例如 {**slug}"
  • catch-all 路由無法比對應該相符的要求。
  • 移除其他路由讓 catch-all 路由開始運作。

如需發生此錯誤 (bug) 的範例案例,請參閱 GitHub 錯誤 (bug) 1867716579

這個錯誤 (bug) 的加入修正包含在 .NET Core 3.1.301 SDK 和更新版本。 下列程式碼會設定修正此錯誤 (bug) 的內部參數:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

全部擷取參數也可以符合空字串。

當使用路由產生 URL (包括路徑分隔符號 / 字元) 時,catch-all 參數會逸出適當的字元。 例如,路由值為 { path = "my/path" } 的路由 foo/{*path} 會產生 foo/my%2Fpath。 請注意逸出的斜線。 若要反覆存取路徑分隔符號字元,請使用 ** 路由參數前置詞。 具有 { path = "my/path" } 的路由 foo/{**path} 會產生 foo/my/path

URL 模式嘗試擷取具有選擇性副檔名的檔案名稱時,具有其他考量。 以範本 files/{filename}.{ext?} 為例。 當 filenameext 都存在值時,就會填入這兩個值。 如果 URL 中只有 filename 的值存在,路由會比對,因為尾端 . 是選擇性的。 下列 URL 符合此路由:

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

路由參數可能有「預設值」,指定方法是在參數名稱之後指定預設值,並以等號 (=) 分隔。 例如,{controller=Home} 定義 Home 作為 controller 的預設值。 如果 URL 中沒有用於參數的任何值,則會使用預設值。 路由參數也可以設為選擇性,方法是在參數名稱結尾附加問號 (?)。 例如: id? 。 選擇性值與預設路由參數之間的差異如下:

  • 具有預設值的路由參數一律會產生值。
  • 選擇性參數只有在要求 URL 提供值時才具有值。

路由參數可能具有條件約束,這些條件約束必須符合與 URL 繫結的路由值。 在路由參數名稱後面新增 : 和條件約束名稱,以指定路由參數上的內嵌條件約束。 如果條件約束需要引數,這些引數會在條件約束名稱後面以括弧 (...) 括住。 您可以附加另一個 : 和條件約束名稱,以指定多個內嵌條件約束

條件約束名稱和引述會傳遞至 IInlineConstraintResolver 服務來建立 IRouteConstraint 的執行個體,以用於 URL 處理。 例如,路由範本 blog/{article:minlength(10)} 指定具有引數 10minlength 條件約束。 如需路由條件約束和架構所提供之條件約束清單的詳細資訊,請參閱路由條件約束一節。

路由參數也可能有參數轉換器。 參數轉換器會在產生連結和比對動作和頁面至 URL 時,轉換參數的值。 如同條件約束,參數轉換器可以藉由在路由參數名稱後面新增 : 和轉換器名稱,以內嵌方式新增至路由參數。 例如,路由範本 blog/{article:slugify} 會指定 slugify 轉換器。 如需參數轉換器的詳細資訊,請參閱參數轉換器一節。

下表示範範例路由範本及其行為:

路由範本 範例比對 URI 要求 URI
hello /hello 只比對單一路徑 /hello
{Page=Home} / 比對並將 Page 設定為 Home
{Page=Home} /Contact 比對並將 Page 設定為 Contact
{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
{controller=Home}/{action=Index}/{id?} /Products 對應至 Products 控制器和 Index 方法。 已忽略 id

使用範本通常是最簡單的路由方式。 條件約束和預設值也可以在路由範本外部指定。

複雜區段

複雜區段會以非窮盡方式,比對從右至左的常值分隔符號來處理。 例如,[Route("/a{b}c{d}")] 是複雜的區段。 複雜區段會以特定方式運作,必須了解才能成功使用這些區段。 本節中的範例示範當分隔符號文字未出現在參數值內時,複雜區段才真正正常運作的原因。 使用 RegEx,然後手動擷取更複雜的案例需要值。

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

這是路由使用範本執行的步驟摘要,/a{b}c{d} 和 URL 路徑 /abcd| 可用來協助視覺化演算法的運作方式:

  • 第一個常值,由右至左,是 c。 因此,/abcd 會從右搜尋,並尋找 /ab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/ab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 沒有剩餘的文字,也沒有剩餘的路由範本,所以這是相符的。

以下是使用相同範本 /a{b}c{d} 和 URL 路徑 /aabcd 的負案例範例。 | 可用來協助視覺化演算法的運作方式。 此案例與相同演算法所說明的不相符:

  • 第一個常值,由右至左,是 c。 因此,/aabcd 會從右搜尋,並尋找 /aab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/aab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /a|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 此時有剩餘的文字 a,但演算法已用盡路由範本來剖析,因此這不是相符項目。

由於比對演算法非窮盡

  • 它會比對每個步驟中可能最少的文字量。
  • 在參數值內出現分隔符號值的任何情況,都會導致不相符。

正則運算式可更充分地控制其比對行為。

窮盡比對,也稱為延遲比對,符合最大的可能字串。 非窮盡符合最小的可能字串。

具有特殊字元的路由

使用特殊字元的路由可能會導致非預期的結果。 例如,請考慮使用下列動作方法的控制器:

[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}

string id 包含下列編碼值時,可能會發生非預期的結果:

ASCII 已編碼
/ %2F
+

路由參數不一定會解碼 URL。 未來可能會解決此問題。 如需詳細資訊,請參閱這個 GitHub 問題

路由條件約束

路由條件約束執行時機是出現符合傳入 URL 的項目,並將 URL 路徑語彙基元化成路由值時。 路由條件約束通常會透過路由範本檢查相關聯的路由值,並針對值是否可接受做出 [True] 或 [False] 的決策。 某些路由條件約束會使用路由值以外的資料,以考慮是否可以路由要求。 例如,HttpMethodRouteConstraint 可以依據其 HTTP 指令動詞接受或拒絕要求。 條件約束可用於路由要求和連結產生。

警告

請勿針對輸入驗證使用條件約束。 如果條件約束用於輸入驗證,則不正確輸入會導致 404 找不到回應。 不正確輸入應該會產生 400 錯誤要求,並顯示適當的錯誤訊息。 路由條件約束會用來釐清類似的路由,而不是用來驗證特定路由的輸入。

下表示範範例路由條件約束及其預期行為:

條件約束 範例 範例相符項目 備註
int {id:int} 123456789-123456789 符合任何整數
bool {active:bool} trueFALSE 比對 truefalse。 不區分大小寫
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm 比對不因文化特性而異中的有效 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 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

以冒號分隔的多個條件約束,可以套用至單一參數。 例如,下列條件約束會將參數限制在 1 或更大的整數值:

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

警告

驗證 URL 並轉換成 CLR 類型的路由條件約束一律使用不因文化特性而異。 例如,轉換成 CLR 類型 intDateTime。 這些條件約束假設 URL 無法當地語系化。 架構提供的路由條件約束不會修改路由值中儲存的值。 所有從 URL 剖析而來的路由值會儲存為字串。 例如,float 條件約束會嘗試將路由值轉換成浮點數,但轉換的值只能用來確認它可以轉換成浮點數。

條件約束中的規則運算式

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

規則運算式可以使用 regex(...) 路由條件約束來指定為內嵌條件約束。 MapControllerRoute 系列中的方法也會接受條件約束的物件常值。 如果使用該表單,字串值會解譯為規則運算式。

下列程式碼使用內嵌 RegEx 條件約束:

app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");

下列程式碼會使用物件常值來指定 RegEx 條件約束:

app.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}$,請使用下列其中一項:

  • 將字串中提供的 \ 字元取代為 C# 來源檔案中的 \\ 字元,以逸出 \ 字串逸出字元。
  • 逐字字串常值

若要逸出路由參數分隔符號 {}[]、運算式中的字元,例如,{{}}[[]]。 下表顯示規則運算式及其逸出的版本:

規則運算式 逸出的規則運算式
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

路由中使用的規則運算式通常會以 ^ 字元開頭,並符合字串的起始位置。 運算式通常會以 $ 字元結尾,並符合字串結尾。 ^$ 字元可確保規則運算式符合整個路由參數值。 如果沒有 ^$ 個字元,規則運算式會比對字串內的任何子字串,這通常是不想要的。 下表提供範例,並說明它們符合或無法符合的原因:

運算式 String 比對 註解
[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 規則運算式

若要將參數限制為一組已知的可能值,請使用規則運算式。 例如,{action:regex(^(list|get|create)$)} 只會將 action 路由值與 listgetcreate 相符。 如果已傳入條件約束字典,字串 ^(list|get|create)$ 則是對等項目。 在不符合其中一個已知條件約束的條件約束字典中傳遞的條件約束也會被視為規則運算式。 在不符合其中一個已知條件約束的範本內傳遞的條件約束不會被視為規則運算式。

自訂路由條件約束

您可以實作 IRouteConstraint 介面來建立自訂路由條件約束。 IRouteConstraint 介面包含 Match,如果滿足條件約束,則會傳回 true,否則會傳回 false

自訂路由條件約束很少需要。 實作自訂路由條件約束之前,請考慮替代專案,例如模型繫結。

ASP.NET Core Constraints 資料夾提供建立條件約束的良好範例。 例如,GuidRouteConstraint

若要使用自訂 IRouteConstraint,路由條件約束類型必須向服務容器中的應用程式 ConstraintMap 註冊。 ConstraintMap 是一個目錄,它將路由限制式機碼對應到可驗證那些限制式的 IRouteConstraint 實作。 應用程式 ConstraintMap 可以在 Program.cs 中更新為 AddRouting 呼叫的一部分,或是直接使用 builder.Services.Configure<RouteOptions> 設定 RouteOptions。 例如:

builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));

上述條件約束會套用在下列程式碼中:

[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}

NoZeroesRouteConstraint 的實作可防止路由參數中使用 0

public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[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 var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

上述 程式碼:

  • 防止路由 {id} 區段中的 0
  • 示範如何提供實作自訂條件約束的基本範例。 它不應該用於生產應用程式。

以下程式碼是防止處理包含 0id 的較佳方法:

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

    return Content(id);
}

上述程式碼在 NoZeroesRouteConstraint 方法上具有下列優點:

  • 它不需要自訂條件約束。
  • 當路由參數包含 0 時,它會傳回更具描述性的錯誤。

參數轉換器

參數轉換程式:

例如,具有 Url.Action(new { article = "MyTestArticle" }) 之路由模式 blog\{article:slugify} 中的自訂 slugify 參數轉換器,會產生 blog\my-test-article

請考慮下列 IOutboundParameterTransformer 實作:

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

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

若要在路由模式中使用參數轉換器,請使用 Program.cs 中的 ConstraintMap 進行設定:

builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));

ASP.NET Core 架構會使用參數轉換器來轉換端點解析的 URI。 例如,參數轉換器會轉換用來比對 areacontrolleractionpage 的路由值:

app.MapControllerRoute(
    name: "default",
    pattern: "{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 頁面。 如需詳細資訊,請參閱控制器中的路由

URL 產生流程會從呼叫 LinkGenerator.GetPathByAddress 或類似的方法開始。 方法會提供位址、一組路由值,以及選擇性地從 HttpContext 取得目前要求的相關資訊。

第一個步驟是使用位址來解析一組候選端點,其 IEndpointAddressScheme<TAddress> 符合位址的類型。

一旦位址配置找到一組候選項目之後,端點會逐一排序並反復處理,直到 URL 產生作業成功為止。 URL 產生不會檢查模棱兩可,傳回的第一個結果會是最終結果。

使用記錄對 URL 產生進行疑難排解

疑難排解 URL 產生的第一個步驟是將 Microsoft.AspNetCore.Routing 的記錄層級設定為 TRACELinkGenerator 記錄其處理的許多詳細資料,這對於疑難排解問題很有用。

如需 URL 產生的詳細資訊,請參閱 URL 產生參考

地址

位址是 URL 產生中用來將呼叫繫結至連結產生器至一組候選端點的概念。

位址是預設隨附兩個實作的可延伸概念:

  • 使用端點名稱 (string) 作為位址:
    • 提供與 MVC 路由名稱類似的功能。
    • 使用 IEndpointNameMetadata 中繼資料類型。
    • 針對所有已註冊端點的中繼資料解析提供的字串。
    • 如果多個端點使用相同的名稱,則會在啟動時擲回例外狀況。
    • 建議在控制器和 Razor 頁面外部使用一般用途。
  • 使用路由值 (RouteValuesAddress) 作為位址:
    • 提供與控制器和 Razor 頁面舊版 URL 產生類似的功能。
    • 擴充和偵錯非常複雜。
    • 提供 IUrlHelper、標籤協助程式、HTML 協助程式、動作結果等所使用的實作。

位址配置的角色是依任意準則在位址與相符端點之間建立關聯:

  • 端點名稱配置會執行基本字典查閱。
  • 路由值配置具有複雜的集合演算法子集。

環境值和明確值

從目前的要求,路由會存取目前要求的路由值 HttpContext.Request.RouteValues。 與目前要求相關聯的值稱為環境值。 為了清楚起見,文件將傳入方法的路由值稱為明確值

下列範例顯示環境值和明確值。 它提供目前要求的環境值和明確值:

public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...

上述 程式碼:

下列程式碼只提供明確值,而且沒有環境值:

var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;

上述方法會傳回 /Home/Subscribe/17

WidgetController 中的下列程式碼會傳回 /Widget/Subscribe/17

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

下列程式碼會從目前要求中的環境值和明確值提供控制器:

public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}

在上述程式碼中:

  • 傳回 /Gadget/Edit/17
  • Url 取得 IUrlHelper
  • Action 會產生具有動作方法絕對路徑的 URL。 URL 包含指定的 action 名稱和 route 值。

下列程式碼提供來自目前要求的環境值和明確值:

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

        // ...
    }
}

上述程式碼會將編輯 Razor 頁面包含下列頁面指示詞時,將 url 設定為 /Edit/17

@page "{id:int}"

如果 [編輯] 頁面不包含 "{id:int}" 路由範本,url 則為 /Edit?id=17

MVC IUrlHelper 的行為除了此處所述的規則之外,也會新增一層複雜度:

  • IUrlHelper 一律提供目前要求的路由值作為環境值。
  • 除非開發人員覆寫,否則 IUrlHelper.Action 一律會將目前 action 值和 controller 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會將目前的 page 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會覆寫目前 handler 路由值,其中 null 做為明確值。

使用者通常對環境值的行為詳細資料感到驚訝,因為 MVC 似乎沒有遵循自己的規則。 基於歷程記錄和相容性考慮,某些路由值,例如 actioncontrollerpagehandler 有自己的特殊案例行為。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage 所提供的對等功能會複製這些 IUrlHelper 的異常,以取得相容性。

URL 產生流程

找到一組候選端點之後,URL 產生演算法:

  • 反復處理端點。
  • 傳回第一個成功的結果。

此程式中的第一個步驟稱為路由值無效。 路由值失效是路由決定應該使用環境值和應忽略哪些路由值的流程。 系統會考慮每個環境值,並結合明確值或忽略。

思考環境值角色的最佳方式是,他們嘗試在一些常見情況下儲存應用程式開發人員輸入。 傳統上,環境值有用的案例與 MVC 相關:

  • 連結至相同控制器中的另一個動作時,不需要指定控制器名稱。
  • 連結至相同區域中的另一個控制器時,不需要指定區功能變數名稱。
  • 連結至相同的動作方法時,不需要指定路由值。
  • 連結至應用程式的另一個部分時,您不想傳遞在應用程式該部分沒有意義的路由值。

對傳回 nullLinkGeneratorIUrlHelper 的呼叫通常是因為無法了解路由值無效所造成。 藉由明確指定更多路由值來針對路由值失效進行疑難排解,以查看這是否可解決問題。

路由值失效的運作方式是假設應用程式的 URL 配置是階層式,而階層是由左至右形成。 請考慮基本控制器路由範本 {controller}/{action}/{id?},以直覺方式了解實際運作方式。 值變更會使所有出現在右邊的路由值失效。 這反映了階層的假設。 如果應用程式有 id 的環境值,則作業會為 controller 指定不同的值:

  • id 不會重複使用,因為 {controller} 位於 {id?} 的左邊。

示範此原則的一些範例:

  • 如果明確值包含 id 的值,則會忽略 id 的環境值。 可以使用 controlleraction 的環境值。
  • 如果明確值包含 action 的值,則會忽略 action 的任何環境值。 可以使用 controller 的環境值。 如果 action 的明確值與 action 的環境值不同,則不會使用 id 值。 如果 action 的明確值與 action 的環境值相同,則可以使用 id 值。
  • 如果明確值包含 controller 的值,則會忽略 controller 的任何環境值。 如果 controller 的明確值與 controller 的環境值不同,則不會使用 actionid 值。 如果 controller 的明確值與 controller 的環境值相同,則可以使用 actionid 值。

透過屬性路由和專用傳統路由的存在,此流程會進一步複雜。 控制器傳統路由,例如 {controller}/{action}/{id?} 使用路由參數指定階層。 針對專用傳統路由屬性路由控制器和 Razor 頁面:

  • 路由值有階層。
  • 它們不會出現在範本中。

在這些情況下,URL 產生會定義必要值概念。 控制器和 Razor 頁面所建立的端點具有指定的必要值,可讓路由值失效運作。

路由值無效演算法詳細資料:

  • 必要的值名稱會與路由參數結合,然後從左至右進行處理。
  • 針對每個參數,會比較環境值和明確值:
    • 如果環境值和明確值相同,流程會繼續。
    • 如果環境值存在且明確值不存在,則產生 URL 時會使用環境值。
    • 如果環境值不存在且明確值存在,請拒絕環境值和所有後續的環境值。
    • 如果環境值和明確值存在,且兩個值不同,請拒絕環境值和所有後續的環境值。

此時,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

路由值不正確問題

下列程式碼顯示路由不支援的 URL 產生配置範例:

app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });

在上述程式碼中,會使用 culture 路由參數進行當地語系化。 想要讓 culture 參數一律接受為環境值。 不過,由於所需值的運作方式,culture 參數不會接受為環境值:

  • "default" 路由範本中,culture 路由參數位於 controller 的左邊,因此 controller 的變更不會使 culture無效。
  • "blog" 路由範本中,culture 路由參數會視為 controller 的右邊,其會出現在必要值中。

使用 LinkParser 剖析 URL 路徑

LinkParser 類別會將剖析 URL 路徑的支援新增至一組路由值。 ParsePathByEndpointName 方法會採用端點名稱和 URL 路徑,並傳回從 URL 路徑擷取的一組路由值。

在下列範例控制器中,GetProduct 動作會使用 api/Products/{id} 的路由範本,並具有 GetProductName

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...

在相同的控制器類別中,AddRelatedProduct 動作需要 URL 路徑,pathToRelatedProduct,其可作為查詢字串參數提供:

[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...

在上述範例中,AddRelatedProduct 動作會從 URL 路徑擷取 id 路由值。 例如,使用 /api/Products/1 的 URL 路徑,relatedProductId 值會設定為 1。 此方法可讓 API 的用戶端在參考資源時使用 URL 路徑,而不需要了解這類 URL 的結構。

設定端點中繼資料

下列連結提供如何設定端點中繼資料的資訊:

搭配 RequireHost 路由中的主機比對

RequireHost 會將條件約束套用至需要指定主機的路由。 RequireHost 或 [Host] 參數可以是:

  • 主機:www.domain.com,符合任何連接埠 www.domain.com
  • 具有萬用字元的主機:*.domain.com、比對任何連接埠上的 www.domain.comsubdomain.domain.comwww.subdomain.domain.com
  • 連接埠:*:5000,符合連接埠 5000 與任何主機。
  • 主機和連接埠:www.domain.com:5000*.domain.com:5000,符合主機和連接埠。

可以使用 RequireHost[Host] 來指定多個參數。 條件約束會比對任何參數有效的主機。 例如,[Host("domain.com", "*.domain.com")] 符合 domain.comwww.domain.comsubdomain.domain.com

下列程式碼會使用 RequireHost 在路由上要求指定的主機:

app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");

下列程式碼會使用控制器上的 [Host] 屬性來要求任何指定的主機:

[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}

[Host] 屬性同時套用至控制器和動作方法時:

  • 會使用動作上的屬性。
  • 控制器屬性會被忽略。

路由的效能指引

當應用程式發生效能問題時,通常會懷疑路由有問題。 路由的原因是控制器和 Razor 頁面等架構會報告架構在其記錄訊息中花費的時間量。 當控制器報告的時間與要求的總時間之間有顯著差異時:

  • 開發人員會將其應用程式程式碼排除為問題的來源。
  • 通常假設路由是原因。

路由是使用數千個端點測試的效能。 一般應用程式不太可能因為太大而遇到效能問題。 路由效能緩慢最常見的根本原因是自訂中介軟體行為不良。

下列程式碼範例示範縮小延遲來源的基本技巧:

var logger = app.Services.GetRequiredService<ILogger<Program>>();

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

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

app.UseRouting();

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

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

app.UseAuthorization();

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

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

app.MapGet("/", () => "Timing Test.");

計時路由:

  • 將每個中介軟體與上述程式碼中顯示的計時中介軟體複本交錯。
  • 新增唯一識別碼,以將計時資料與程式碼相互關聯。

這是一種基本方式,可以在延遲顯著時縮小延遲,例如,超過 10ms。 從 Time 1 減去 Time 2 會報告 UseRouting 中介軟體內所花費的時間。

下列程式碼會使用更精簡的方法來處理上述計時程式碼:

public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

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

app.UseRouting();

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

app.UseAuthorization();

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

app.MapGet("/", () => "Timing Test.");

潛在昂貴的路由功能

下列清單提供相較於基本路由範本相對昂貴之路由功能的一些見解:

  • 規則運算式:可以撰寫複雜的規則運算式,或有少量輸入的長時間執行時間。
  • 複雜區段 ({x}-{y}-{z}):
    • 比剖析一般 URL 路徑區段更昂貴。
    • 導致配置更多子字串。
  • 同步資料存取:許多複雜的應用程式在其路由中具有資料庫存取權。 使用擴充點,例如 MatcherPolicyEndpointSelectorContext,這是非同步。

大型路由表的指引

根據預設,ASP.NET Core 會使用路由演算法來交換 CPU 時間的記憶體。 這有一個良好的效果,路由比對時間只取決於路徑的長度,而不是路由數目。 不過,在某些情況下,當應用程式有大量的路由 (數千個),而且路由中有大量的變數前置詞時,這種方法可能會有問題。 例如,如果路由在路由的早期區段中具有參數,例如 {parameter}/some/literal

除非下列情況,否則應用程式不太可能遇到此問題的情況:

  • 此模式在應用程式中有許多路由。
  • 應用程式中有許多路由。

如何判斷應用程式是否正在發生大型路由表問題

  • 有兩個徵兆需要尋找:
    • 應用程式在第一個要求上啟動的速度很慢。
      • 請注意,這是必要項目,但並不足夠。 還有其他許多非路由問題,可能會導致應用程式啟動緩慢。 檢查下列條件,以正確判斷應用程式是否正在發生這種情況。
    • 應用程式會在啟動期間耗用大量的記憶體,而記憶體傾印會顯示大量 Microsoft.AspNetCore.Routing.Matching.DfaNode 執行個體。

如何解決此問題

有數種技術和最佳化可以套用至大幅改善此案例的路由:

  • 視需要將路由條件約束套用至參數,例如 {parameter:int}{parameter:guid}{parameter:regex(\\d+)} 等。
    • 這可讓路由演算法在內部最佳化用於比對的結構,並大幅減少所使用的記憶體。
    • 在絕大多數情況下,這足以回到可接受的行為。
  • 變更路由以將參數移至範本中的稍後區段。
    • 這樣可減少可能「路徑」的數目,以符合指定路徑的端點。
  • 使用動態路由,並動態執行控制器/頁面的對應。
    • 這可以使用 MapDynamicControllerRouteMapDynamicPageRoute 來達成。

程式庫作者的指引

本節包含程式庫作者建置在路由之上的指引。 這些詳細資料旨在確保應用程式開發人員有良好的使用程式庫和架構來擴充路由的體驗。

定義端點

若要建立使用路由進行 URL 比對的架構,請先定義建置在 UseEndpoints 之上的使用者體驗。

DO 建置在 IEndpointRouteBuilder 之上。 這可讓使用者與其他 ASP.NET Core 功能撰寫架構,而不會造成混淆。 每個 ASP.NET Core 範本都包含路由。 假設使用者有路由存在且熟悉。

// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");

DO 會從實作 IEndpointConventionBuilderMapMyFramework(...) 呼叫傳回密封具體型別。 大部分的架構 Map... 方法都遵循此模式。 IEndpointConventionBuilder 介面:

  • 允許撰寫中繼資料。
  • 以各種擴充方法為目標。

宣告您自己的型別可讓您將自己的架構特定功能新增至建立器。 可以包裝架構宣告的建立器,並將呼叫轉接給它。

// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");

考慮撰寫您自己的 EndpointDataSourceEndpointDataSource 是宣告和更新端點集合的低層級基本類型。 EndpointDataSource 是控制器和 Razor 頁面所使用的強大 API。

路由測試具有非更新資料來源基本範例

根據預設,請勿嘗試註冊 EndpointDataSource。 要求使用者在 UseEndpoints 中註冊您的架構。 路由的理念是預設不會包含任何項目,而且 UseEndpoints 是註冊端點的位置。

建立路由整合式中介軟體

考慮將中繼資料型別定義為介面。

DO 可讓您在類別和方法上使用中繼資料型別作為屬性。

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

控制器和 Razor 頁面等架構支援將中繼資料屬性套用至型別和方法。 如果您宣告中繼資料型別:

  • 將它們設為可存取的屬性
  • 大部分的使用者都熟悉套用屬性。

將中繼資料型別宣告為介面會新增另一層彈性:

  • 介面是可組合的。
  • 開發人員可以宣告自己結合多個原則的型別。

DO 可讓您覆寫中繼資料,如下列範例所示:

[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() { }
}

遵循這些指導方針的最佳方式是避免定義標記中繼資料

  • 不要只尋找中繼資料型別是否存在。
  • 在中繼資料上定義屬性,並檢查屬性。

中繼資料集合是排序的,並支援依優先順序覆寫。 在控制器的情況下,動作方法上的中繼資料最具體。

DO 讓中介軟體在不使用路由的情況下使用:

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

// Your framework
app.MapMyFramework(...).RequireAuthorization();

作為此指導方針的範例,請考慮 UseAuthorization 中介軟體。 授權中介軟體可讓您傳入後援原則。 如果指定後援原則,則適用於兩者:

  • 不含指定原則的端點。
  • 不符合端點的要求。

這可讓授權中介軟體在路由內容之外很有用。 授權中介軟體可用於傳統中介軟體程式設計。

偵錯診斷

如需詳細的路由診斷輸出,請將 Logging:LogLevel:Microsoft 設定為 Debug。 在開發環境中,在 appsettings.Development.json 中設定記錄層級:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

其他資源

路由負責比對連入 HTTP 要求,並將這些要求分派至應用程式的可執行端點。 端點是應用程式的可執行檔要求處理程式碼單位。 端點會在應用程式中定義,並在應用程式啟動時設定。 端點比對程式可以從要求的 URL 擷取值,並提供這些值以供要求處理。 使用來自應用程式的端點資訊,路由也能夠產生對應至端點的 URL。

應用程式可以使用下列方式設定路由:

本文件涵蓋路由 ASP.NET Core 低層級詳細資料。 如需設定路由的資訊:

本文件中所述的端點路由系統適用於 ASP.NET Core 3.0 和更新版本。 如需以 IRouter 為基礎的上一個路由系統資訊,請使用下列其中一種方法選取 ASP.NET Core 2.1 版本:

檢視或下載範例程式碼 \(英文\) (如何下載)

此文件的下載範例是由特定 Startup 類別啟用。 若要執行特定範例,請修改 Program.cs 以呼叫所需的 Startup 類別。

路由的基本概念

所有 ASP.NET Core 範本都包含產生的程式碼中的路由。 路由會在 Startup.Configure 中的中介軟體管線中註冊。

下列程式碼顯示路由的基本範例:

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!");
        });
    });
}

路由使用一對中介軟體,由 UseRoutingUseEndpoints 註冊:

  • UseRouting 會將路由比對新增至中介軟體管線。 此中介軟體會查看應用程式中定義的端點集,並根據要求選取最佳端點比對
  • UseEndpoints 將端點執行新增至中介軟體管線。 它會執行與所選端點相關聯的委派。

上述範例會使用 MapGet 方法,將單一路由傳送至程式碼端點:

  • 當 HTTP GET 要求傳送至根 URL / 時:
    • 顯示的要求委派會執行。
    • Hello World! 會寫入 HTTP 回應。 根據預設,根 URL /https://localhost:5001/
  • 如果要求方法不是 GET 或根 URL 不是 /,則不會比對路由,而且會傳回 HTTP 404。

端點

MapGet 方法可用來定義端點。 端點可以是:

  • 選取,方法是比對 URL 和 HTTP 方法。
  • 執行,透過執行委派。

應用程式可以比對和執行的端點會在 UseEndpoints 中設定。 例如,MapGetMapPost類似的方法,將要求委派連接到路由系統。 其他方法可用來將 ASP.NET Core 架構功能連線至路由系統:

下列範例示範使用更複雜的路由範本進行路由:

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}路由範本。 它用來設定端點的比對方式。 在此情況下,範本會比對:

  • URL,例如 /hello/Ryan
  • 開頭為 /hello/ 後面接著一連串字母字元的任何 URL 路徑。 :alpha 套用只符合字母字元的路由條件約束。 本文件稍後會說明路由條件約束

URL 路徑的第二個區段 {name:alpha}

本文件中所述的端點路由系統是 ASP.NET Core 3.0 的新功能。 不過,ASP.NET Core 的所有版本都支援一組相同的路由範本功能和路由條件約束。

下列範例顯示具有健康狀態檢查和授權的路由:

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!");
        });
    });
}

如果您想要查看翻譯為英文以外語言的程式碼註解,請在此 GitHub 討論問題中告訴我們。

上述範例示範如何:

  • 授權中介軟體可以搭配路由使用。
  • 端點可用來設定授權行為。

MapHealthChecks 呼叫會新增健康情況檢查端點。 鏈結 RequireAuthorization 至此呼叫會將授權原則附加至端點。

呼叫 UseAuthenticationUseAuthorization 以新增驗證和授權中介軟體。 這些中介軟體會放在 UseRoutingUseEndpoints 之間,讓它們能夠:

  • 查看 UseRouting 所選取的端點。
  • UseEndpoints 分派至端點之前,請先套用授權原則。

端點中繼資料

在上述範例中,有兩個端點,但只有健康情況檢查端點已附加授權原則。 如果要求符合健康情況檢查端點 /healthz,則會執行授權檢查。 這示範端點可以附加額外的資料。 此額外資料稱為端點中繼資料

  • 中繼資料可以透過路由感知中介軟體來處理。
  • 中繼資料可以是任何 .NET 類型。

路由概念

路由系統會藉由新增功能強大的端點概念,以建置在中介軟體管線之上。 端點代表應用程式功能單位,這些單位在路由、授權和任何數目 ASP.NET Core 的系統方面彼此不同。

ASP.NET Core 端點定義

ASP.NET Core 端點為:

下列程式碼示範如何擷取和檢查符合目前要求的端點:

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!");
        });
    });
}

如果選取端點,可以從 HttpContext 擷取。 可以檢查其屬性。 端點物件是不可變的,在建立之後無法修改。 最常見的端點類型是 RouteEndpointRouteEndpoint 包含允許路由系統選取的資訊。

在上述程式碼中,app.Use 設定內嵌中介軟體

下列程式碼顯示,視管線中呼叫 app.Use 的位置而定,可能沒有端點:

// 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 語句,以顯示是否已選取端點。 為了清楚起見,此範例會將顯示名稱指派給提供的 / 端點。

執行具有 / URL 的程式碼會顯示:

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

使用任何其他 URL 執行此程式碼會顯示:

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

此輸出示範:

  • 呼叫 UseRouting 之前,端點一律為 null。
  • 如果找到相符專案,則端點在 UseRoutingUseEndpoints 之間為非 null。
  • 找到相符項目時,UseEndpoints 中介軟體是終端。 本文件稍後會定義終端中介軟體
  • 只有在找不到相符項目時,才會執行 UseEndpoints 後的中介軟體。

UseRouting 中介軟體會使用 SetEndpoint 方法,將端點附加至目前的內容。 您可以使用自訂邏輯取代 UseRouting 中介軟體,但仍可取得使用端點的權益。 端點是低層級基本類型,例如中介軟體,不會與路由實作結合。 大部分的應用程式不需要以自訂邏輯取代 UseRouting

UseEndpoints 中介軟體的設計目的是與 UseRouting 中介軟體搭配使用。 執行端點的核心邏輯並不複雜。 使用 GetEndpoint 來擷取端點,然後叫用其 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 之前執行,以修改路由運作的資料。
  • 中介軟體可以在 UseRoutingUseEndpoints 之間執行,以在執行端點之前處理路由的結果。
    • UseRoutingUseEndpoints之間執行的中介軟體:
      • 通常會檢查中繼資料以了解端點。
      • 通常會依照 UseAuthorizationUseCors 來完成安全性決策。
    • 中介軟體和中繼資料的組合可讓您設定每個端點的原則。

上述程式碼示範支援每個端點原則的自訂中介軟體範例。 中介軟體會將敏感性資料存取權的稽核記錄寫入主控台。 中介軟體可以設定為使用 AuditPolicyAttribute 中繼資料端點稽核。 此範例示範加入宣告模式,其中只會稽核標示為敏感性的端點。 您可以反向定義此邏輯,例如稽核未標示為安全的所有內容。 端點中繼資料系統具有彈性。 此邏輯可以設計成任何適合使用案例的方式。

上述範例程式碼旨在示範端點的基本概念。 此範例不適用於生產環境稽核記錄中介軟體的更完整版本會:

  • 登入檔案或資料庫。
  • 包含詳細資料,例如使用者、IP 位址、敏感性端點的名稱等。

稽核原則中繼資料 AuditPolicyAttribute 定義為 Attribute,以便更輕鬆地與控制器和 SignalR 等類別架構搭配使用。 使用 路由至程式碼 時:

  • 中繼資料會與產生器 API 連結。
  • 以類別為基礎的架構會在建立端點時,包含對應方法和類別上的所有屬性。

中繼資料類型的最佳做法是將它們定義為介面或屬性。 介面和屬性允許重複使用程式碼。 中繼資料系統具有彈性,而且不會施加任何限制。

比較終端中介軟體和路由

下列程式碼範例會使用中介軟體與使用路由進行對比:

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!");
        });
    });
}

Approach 1: 顯示的中介軟體樣式為終端中介軟體。 它稱為終端中介軟體,因為它會執行比對作業:

  • 上述範例中的比對作業針對中介軟體是 Path == "/",針對路由是 Path == "/Movie"
  • 比對成功時,它會執行一些功能並傳回,而不是叫用 next 中介軟體。

它稱為終端中介軟體,因為它會終止搜尋、執行某些功能,然後傳回。

比較終端中介軟體和路由:

  • 這兩種方法都允許終止處理管線:
    • 中介軟體會傳回而不是叫用 next 來終止管線。
    • 端點一律為終端。
  • 終端中介軟體允許將中介軟體放置在管線中的任意位置:
  • 終端中介軟體可讓任意程式碼判斷中介軟體何時符合:
    • 自訂路由比對程式碼可能很詳細且難以正確撰寫。
    • 路由為一般應用程式提供直接的解決方案。 大部分的應用程式都不需要自訂路由比對程式碼。
  • 端點介面與中介軟體,例如 UseAuthorizationUseCors
    • 使用具有 UseAuthorizationUseCors 的終端中介軟體需要手動與授權系統互動。

端點定義兩者:

  • 處理要求的委派。
  • 任意中繼資料的集合。 中繼資料是用來根據附加至每個端點的原則和組態來實作跨領域考量。

終端中介軟體可以是有效的工具,但可能需要:

  • 大量的程式碼撰寫和測試。
  • 手動與其他系統整合,以達到所需的彈性層級。

在撰寫終端中介軟體之前,請考慮與路由整合。

MapMapWhen 整合的現有終端中介軟體通常可以轉換成路由感知端點。 MapHealthChecks 示範路由器軟體的模式:

下列程式碼示範如何使用 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!");
        });
    });
}

上述範例顯示為何傳回建立器物件很重要。 傳回建立器物件可讓應用程式開發人員設定原則,例如端點的授權。 在此範例中,健康情況檢查中介軟體與授權系統沒有直接整合。

中繼資料系統是針對使用終端中介軟體的擴充性建立者所遇到的問題而建立的。 每個中介軟體實作自己的與授權系統的整合是有問題的。

URL 比對

  • 這是路由比對傳入要求與端點的流程。
  • 以 URL 路徑和標頭中的資料為基礎。
  • 可以擴充以考慮要求中的任何資料。

當路由中介軟體執行時,它會設定 Endpoint,並將值路由至目前要求 HttpContext 上的要求功能

在路由中介軟體可以檢查端點並採取動作之後,中介軟體執行。 例如,授權中介軟體可以詢問授權原則的端點中繼資料集合。 執行要求處理管線中的所有中介軟體之後,會叫用所選端點的委派。

端點路由中的路由系統負責制定所有分派決策。 因為中介軟體會根據選取的端點套用原則,因此請務必:

  • 任何可能影響分派或安全性原則應用的決定,都會在路由系統內進行。

警告

為了回溯相容性,當執行 Controller 或 Razor頁面端點委派時,RouteContext.RouteData 的屬性會根據到目前為止所執行的要求處理,設定為適當的值。

RouteContext 類型將在未來的版本中標示為過時:

  • RouteData.Values 移轉至 HttpRequest.RouteValues
  • 移轉 RouteData.DataTokens 以從端點中繼資料擷取 IDataTokensMetadata

URL 比對會在一組可設定的階段中運作。 在每個階段中,輸出是一組相符項目。 下一個階段可以進一步縮小一組相符項目。 路由實作不保證相符端點的處理順序。 所有可能的相符項目都會一次處理。 URL 比對階段會依下列順序進行。 ASP.NET Core:

  1. 針對一組端點及其路由範本處理 URL 路徑,並收集所有相符項目。
  2. 採用上述清單,並移除套用路由條件約束失敗的相符項目。
  3. 採用上述清單,並移除失敗 MatcherPolicy 執行個體集的相符項目。
  4. 使用 EndpointSelector 以從上述清單做出最終決策。

端點清單會根據下列項目排定優先順序:

每個階段都會處理所有相符的端點,直到到達 EndpointSelector 為止。 EndpointSelector 是最後階段。 它會從相符項目中選擇最高的優先順序端點作為最佳相符項目。 如果其他相符項目具有與最佳相符項目相同的優先順序,則會擲回模棱兩可的比對例外狀況。

路由優先順序是根據更具體路由範本來計算,其優先順序較高。 例如,請考慮範本 /hello/{message}

  • 兩者都符合 URL 路徑 /hello
  • /hello 更具體,因此優先順序較高。

一般而言,路由優先順序會針對實際使用的 URL 配置類型,選擇最相符的作業。 只有在需要時才使用 Order 以避免模棱兩可。

由於路由所提供的擴充性種類,路由系統無法事先計算模棱兩可的路由。 請考慮路由範本 /{message:alpha}/{message:int} 等範例:

  • alpha 條件約束只符合字母字元。
  • int 條件約束只符合數字。
  • 這些範本具有相同的路由優先順序,但兩者都沒有相符的單一 URL。
  • 如果路由系統在啟動時回報模棱兩可的錯誤,則會封鎖此有效的使用案例。

警告

UseEndpoints 內的作業順序不會影響路由的行為,但有一個例外狀況。 MapControllerRouteMapAreaRoute 會根據叫用的順序,自動將訂單值指派給其端點。 這會模擬控制器的長期行為,而沒有路由系統提供與舊版路由實作相同的保證。

在舊版路由實作中,您可以實作路由擴充性,其相依於路由的處理順序。 ASP.NET Core 3.0 和更新版本中的端點路由:

  • 沒有路由的概念。
  • 不提供訂購保證。 所有端點都會一次處理。

路由範本優先順序和端點選取順序

路由範本優先順序是一個系統,會根據路由範本的特定程度,為每個路由範本指派值。 路由範本優先順序:

  • 避免在常見情況下調整端點的順序。
  • 嘗試比對路由行為的常識預期。

例如,請考慮範本 /Products/List/Products/{id}。 對於 URL 路徑 /Products/List,可以合理地假設 /Products/List/Products/{id} 更相符。 這可運作,因為常值區段 /List 的優先順序會比參數區段 /{id} 更好。

優先順序運作方式的詳細資料會與如何定義路由範本:

  • 具有更多區段的範本會被視為更具體。
  • 具有常值文字的區段會比參數區段更具體。
  • 具有條件約束的參數區段會被視為比不具條件的參數區段更具體。
  • 複雜區段會被視為具有條件約束的參數區段。
  • Catch-all 參數是最不具體的。 如需 catch-all 路由的重要資訊,請參閱路由範本參考中的 catch-all

如需確切值的參考,請參閱 GitHub 上的原始程式碼

URL 產生概念

URL 產生:

  • 這是路由可以根據一組路由值建立 URL 路徑的流程。
  • 允許在端點與存取端點的 URL 之間進行邏輯分隔。

端點路由包含 LinkGenerator API。 LinkGenerator 是單一資料庫,可從 DI 取得。 LinkGenerator API 可以在執行要求的內容之外使用。 Mvc.IUrlHelper 和依賴 IUrlHelper 的情節,例如標籤協助程式、HTML 協助程式,以及動作結果,請在內部使用 LinkGenerator API 來提供連結產生功能。

連結產生器背後支援的概念為「位址」和「位址配置」。 位址配置可讓您判斷應考慮用於連結產生的端點。 例如,路由名稱和路由值情節,許多使用者都熟悉來自控制器,Razor 頁面會實作為位址配置。

連結產生器可以透過下列擴充方法連結至控制器和 Razor 頁面:

這些方法的多載會接受包含 HttpContext 的引數。 這些方法的功能相當於 Url.ActionUrl.Page,但提供額外的彈性和選項。

GetPath* 方法最類似 Url.ActionUrl.Page,因為它們會產生包含絕對路徑的 URI。 GetUri* 方法一律會產生包含配置和主機的絕對 URI。 接受 HttpContext 的方法會在執行要求的內容中產生 URI。 除非覆寫,否則會使用來自執行要求的環境路由值、URL 基底路徑、配置和主機。

呼叫 LinkGenerator 並指定一個位址。 執行下列兩個步驟來產生 URI:

  1. 將位址繫結至符合該位址的端點清單。
  2. 評估每個端點的 RoutePattern,直到找到符合所提供值的路由模式。 產生的輸出會與提供給連結產生器的其他 URI 組件合併並傳回。

LinkGenerator 提供的方法支援適用於任何位址類型的標準連結產生功能。 使用連結產生器的最便利方式是透過執行特定位址類型作業的擴充方法:

擴充方法 描述
GetPathByAddress 根據提供的值產生具有絕對路徑的 URI。
GetUriByAddress 根據提供的值產生絕對 URI。

警告

注意呼叫 LinkGenerator 方法的下列影響:

  • 使用 GetUri* 擴充方法,並注意應用程式組態不會驗證傳入要求的 Host 標頭。 如果傳入要求的 Host 標頭未經驗證,則可能將未受信任的要求輸入傳回檢視或頁面 URI 中的用戶端。 建議所有生產應用程式將其伺服器設定為驗證 Host 標頭是否為已知有效值。

  • 使用 LinkGenerator,並注意與 MapMapWhen 搭配使用的中介軟體。 Map* 會變更執行要求的基底路徑,這會影響連結產生的輸出。 所有的 LinkGenerator API 都允許指定基底路徑。 指定空基底路徑,以復原 Map* 對連結產生的影響。

中介軟體範例

在下列範例中,中介軟體會使用 LinkGenerator API 來建立列出市集產品的動作方法連結。 應用程式中的任何類別都可以使用連結產生器,方法是將它插入類別並呼叫 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 路徑的解碼標記法。 若要比對常值路由參數分隔符號 {},請重複字元來逸出分隔符號。 例如 {{}}

星號 * 或雙星號 **

  • 可作為路由參數的前置詞,以繫結至 URI 的其餘部分。
  • 稱為 catch-all 參數。 例如 blog/{**slug}
    • 比對開頭為 /blog 的任何 URI,並在它之後具有任何值。
    • 下列 /blog 值會指派給路由值 slug

警告

由於路由中的錯誤 (bug)catch-all 參數可能會錯誤比對路由。 受到此錯誤 (bug) 影響的應用程式具有下列特性:

  • catch-all 路由,例如 {**slug}"
  • catch-all 路由無法比對應該相符的要求。
  • 移除其他路由讓 catch-all 路由開始運作。

如需發生此錯誤 (bug) 的範例案例,請參閱 GitHub 錯誤 (bug) 1867716579

這個錯誤 (bug) 的加入修正包含在 .NET Core 3.1.301 SDK 和更新版本。 下列程式碼會設定修正此錯誤 (bug) 的內部參數:

public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.

全部擷取參數也可以符合空字串。

當使用路由產生 URL (包括路徑分隔符號 / 字元) 時,catch-all 參數會逸出適當的字元。 例如,路由值為 { path = "my/path" } 的路由 foo/{*path} 會產生 foo/my%2Fpath。 請注意逸出的斜線。 若要反覆存取路徑分隔符號字元,請使用 ** 路由參數前置詞。 具有 { path = "my/path" } 的路由 foo/{**path} 會產生 foo/my/path

URL 模式嘗試擷取具有選擇性副檔名的檔案名稱時,具有其他考量。 以範本 files/{filename}.{ext?} 為例。 當 filenameext 都存在值時,就會填入這兩個值。 如果 URL 中只有 filename 的值存在,路由會比對,因為尾端 . 是選擇性的。 下列 URL 符合此路由:

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

路由參數可能有「預設值」,指定方法是在參數名稱之後指定預設值,並以等號 (=) 分隔。 例如,{controller=Home} 定義 Home 作為 controller 的預設值。 如果 URL 中沒有用於參數的任何值,則會使用預設值。 路由參數也可以設為選擇性,方法是在參數名稱結尾附加問號 (?)。 例如: id? 。 選擇性值與預設路由參數之間的差異如下:

  • 具有預設值的路由參數一律會產生值。
  • 選擇性參數只有在要求 URL 提供值時才具有值。

路由參數可能具有條件約束,這些條件約束必須符合與 URL 繫結的路由值。 在路由參數名稱後面新增 : 和條件約束名稱,以指定路由參數上的內嵌條件約束。 如果條件約束需要引數,這些引數會在條件約束名稱後面以括弧 (...) 括住。 您可以附加另一個 : 和條件約束名稱,以指定多個內嵌條件約束

條件約束名稱和引述會傳遞至 IInlineConstraintResolver 服務來建立 IRouteConstraint 的執行個體,以用於 URL 處理。 例如,路由範本 blog/{article:minlength(10)} 指定具有引數 10minlength 條件約束。 如需路由條件約束詳細資訊和架構所提供的條件約束清單,請參閱路由條件約束參考一節。

路由參數也可能有參數轉換器。 參數轉換器會在產生連結和比對動作和頁面至 URL 時,轉換參數的值。 如同條件約束,參數轉換器可以藉由在路由參數名稱後面新增 : 和轉換器名稱,以內嵌方式新增至路由參數。 例如,路由範本 blog/{article:slugify} 會指定 slugify 轉換器。 如需參數轉換器的詳細資訊,請參閱參數轉器參考一節。

下表示範範例路由範本及其行為:

路由範本 範例比對 URI 要求 URI
hello /hello 只比對單一路徑 /hello
{Page=Home} / 比對並將 Page 設定為 Home
{Page=Home} /Contact 比對並將 Page 設定為 Contact
{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
{controller=Home}/{action=Index}/{id?} /Products 對應至 Products 控制器和 Index 方法。 已忽略 id

使用範本通常是最簡單的路由方式。 條件約束和預設值也可以在路由範本外部指定。

複雜區段

複雜區段會以非窮盡方式,比對從右至左的常值分隔符號來處理。 例如,[Route("/a{b}c{d}")] 是複雜的區段。 複雜區段會以特定方式運作,必須了解才能成功使用這些區段。 本節中的範例示範當分隔符號文字未出現在參數值內時,複雜區段才真正正常運作的原因。 使用 RegEx,然後手動擷取更複雜的案例需要值。

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

這是路由使用範本執行的步驟摘要,/a{b}c{d} 和 URL 路徑 /abcd| 可用來協助視覺化演算法的運作方式:

  • 第一個常值,由右至左,是 c。 因此,/abcd 會從右搜尋,並尋找 /ab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/ab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 沒有剩餘的文字,也沒有剩餘的路由範本,所以這是相符的。

以下是使用相同範本 /a{b}c{d} 和 URL 路徑 /aabcd 的負案例範例。 | 可用來協助視覺化演算法的運作方式。 此案例與相同演算法所說明的不相符:

  • 第一個常值,由右至左,是 c。 因此,/aabcd 會從右搜尋,並尋找 /aab|c|d
  • 右側 (d) 的所有項目現在都與路由參數 {d} 相符。
  • 下一個常值,由右至左,是 a。 因此,/aab|c|d 會從我們離開的位置開始搜尋,然後 a 找到 /a|a|b|c|d
  • 右邊的值 (b) 現在會比對路由參數 {b}
  • 此時有剩餘的文字 a,但演算法已用盡路由範本來剖析,因此這不是相符項目。

由於比對演算法非窮盡

  • 它會比對每個步驟中可能最少的文字量。
  • 在參數值內出現分隔符號值的任何情況,都會導致不相符。

正則運算式可更充分地控制其比對行為。

窮盡比對,也稱為延遲比對,符合最大的可能字串。 非窮盡符合最小的可能字串。

路由條件約束參考

路由條件約束執行時機是出現符合傳入 URL 的項目,並將 URL 路徑語彙基元化成路由值時。 路由條件約束通常會透過路由範本檢查相關聯的路由值,並針對值是否可接受做出 [True] 或 [False] 的決策。 某些路由條件約束會使用路由值以外的資料,以考慮是否可以路由要求。 例如,HttpMethodRouteConstraint 可以依據其 HTTP 指令動詞接受或拒絕要求。 條件約束可用於路由要求和連結產生。

警告

請勿針對輸入驗證使用條件約束。 如果條件約束用於輸入驗證,則不正確輸入會導致 404 找不到回應。 不正確輸入應該會產生 400 錯誤要求,並顯示適當的錯誤訊息。 路由條件約束會用來釐清類似的路由,而不是用來驗證特定路由的輸入。

下表示範範例路由條件約束及其預期行為:

條件約束 範例 範例相符項目 備註
int {id:int} 123456789-123456789 符合任何整數
bool {active:bool} trueFALSE 比對 truefalse。 不區分大小寫
datetime {dob:datetime} 2016-12-312016-12-31 7:32pm 比對不因文化特性而異中的有效 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 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

以冒號分隔的多個條件約束,可以套用至單一參數。 例如,下列條件約束會將參數限制在 1 或更大的整數值:

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

警告

驗證 URL 並轉換成 CLR 類型的路由條件約束一律使用不因文化特性而異。 例如,轉換成 CLR 類型 intDateTime。 這些條件約束假設 URL 無法當地語系化。 架構提供的路由條件約束不會修改路由值中儲存的值。 所有從 URL 剖析而來的路由值會儲存為字串。 例如,float 條件約束會嘗試將路由值轉換成浮點數,但轉換的值只能用來確認它可以轉換成浮點數。

條件約束中的規則運算式

警告

使用 System.Text.RegularExpressions 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

規則運算式可以使用 regex(...) 路由條件約束來指定為內嵌條件約束。 MapControllerRoute 系列中的方法也會接受條件約束的物件常值。 如果使用該表單,字串值會解譯為規則運算式。

下列程式碼使用內嵌 RegEx 條件約束:

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

下列程式碼會使用物件常值來指定 RegEx 條件約束:

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}$,請使用下列其中一項:

  • 將字串中提供的 \ 字元取代為 C# 來源檔案中的 \\ 字元,以逸出 \ 字串逸出字元。
  • 逐字字串常值

若要逸出路由參數分隔符號 {}[]、運算式中的字元,例如,{{}}[[]]。 下表顯示規則運算式及其逸出的版本:

規則運算式 逸出的規則運算式
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$

路由中使用的規則運算式通常會以 ^ 字元開頭,並符合字串的起始位置。 運算式通常會以 $ 字元結尾,並符合字串結尾。 ^$ 字元可確保規則運算式符合整個路由參數值。 如果沒有 ^$ 個字元,規則運算式會比對字串內的任何子字串,這通常是不想要的。 下表提供範例,並說明它們符合或無法符合的原因:

運算式 String 比對 註解
[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 規則運算式

若要將參數限制為一組已知的可能值,請使用規則運算式。 例如,{action:regex(^(list|get|create)$)} 只會將 action 路由值與 listgetcreate 相符。 如果已傳入條件約束字典,字串 ^(list|get|create)$ 則是對等項目。 在不符合其中一個已知條件約束的條件約束字典中傳遞的條件約束也會被視為規則運算式。 在不符合其中一個已知條件約束的範本內傳遞的條件約束不會被視為規則運算式。

自訂路由條件約束

您可以實作 IRouteConstraint 介面來建立自訂路由條件約束。 IRouteConstraint 介面包含 Match,如果滿足條件約束,則會傳回 true,否則會傳回 false

自訂路由條件約束很少需要。 實作自訂路由條件約束之前,請考慮替代專案,例如模型繫結。

ASP.NET Core Constraints 資料夾提供建立條件約束的良好範例。 例如,GuidRouteConstraint

若要使用自訂 IRouteConstraint,路由條件約束類型必須向服務容器中的應用程式 ConstraintMap 註冊。 ConstraintMap 是一個目錄,它將路由限制式機碼對應到可驗證那些限制式的 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.RouteInfo 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 來處理不受信任的輸入時,請傳遞逾時。 惡意使用者可以提供輸入給 RegularExpressions,導致拒絕服務 (DoS) 攻擊。 使用 RegularExpressions 的 ASP.NET Core 架構 API 會傳遞逾時。

上述 程式碼:

  • 防止路由 {id} 區段中的 0
  • 示範如何提供實作自訂條件約束的基本範例。 它不應該用於生產應用程式。

以下程式碼是防止處理包含 0id 的較佳方法:

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

    return ControllerContext.MyDisplayRouteInfo(id);
}

上述程式碼在 MyCustomConstraint 方法上具有下列優點:

  • 它不需要自訂條件約束。
  • 當路由參數包含 0 時,它會傳回更具描述性的錯誤。

參數轉換器參考

參數轉換程式:

例如,具有 Url.Action(new { article = "MyTestArticle" }) 之路由模式 blog\{article:slugify} 中的自訂 slugify 參數轉換器,會產生 blog\my-test-article

請考慮下列 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 架構會使用參數轉換器來轉換端點解析的 URI。 例如,參數轉換器會轉換用來比對 areacontrolleractionpage 的路由值。

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 頁面。 如需詳細資訊,請參閱控制器中的路由

URL 產生程式會從呼叫 LinkGenerator.GetPathByAddress 或類似的方法開始。 方法會提供位址、一組路由值,以及選擇性地從 HttpContext 取得目前要求的相關資訊。

第一個步驟是使用位址來解析一組候選端點,其 IEndpointAddressScheme<TAddress> 符合位址的類型。

一旦位址配置找到一組候選項目之後,端點會逐一排序並反復處理,直到 URL 產生作業成功為止。 URL 產生不會檢查模棱兩可,傳回的第一個結果會是最終結果。

使用記錄對 URL 產生進行疑難排解

疑難排解 URL 產生的第一個步驟是將 Microsoft.AspNetCore.Routing 的記錄層級設定為 TRACELinkGenerator 記錄其處理的許多詳細資料,這對於疑難排解問題很有用。

如需 URL 產生的詳細資訊,請參閱 URL 產生參考

地址

位址是 URL 產生中用來將呼叫繫結至連結產生器至一組候選端點的概念。

位址是預設隨附兩個實作的可延伸概念:

  • 使用端點名稱 (string) 作為位址:
    • 提供與 MVC 路由名稱類似的功能。
    • 使用 IEndpointNameMetadata 中繼資料類型。
    • 針對所有已註冊端點的中繼資料解析提供的字串。
    • 如果多個端點使用相同的名稱,則會在啟動時擲回例外狀況。
    • 建議在控制器和 Razor 頁面外部使用一般用途。
  • 使用路由值 (RouteValuesAddress) 作為位址:
    • 提供與控制器和 Razor 頁面舊版 URL 產生類似的功能。
    • 擴充和偵錯非常複雜。
    • 提供 IUrlHelper、標籤協助程式、HTML 協助程式、動作結果等所使用的實作。

位址配置的角色是依任意準則在位址與相符端點之間建立關聯:

  • 端點名稱配置會執行基本字典查閱。
  • 路由值配置具有複雜的集合演算法子集。

環境值和明確值

從目前的要求,路由會存取目前要求的路由值 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);
    }

上述 程式碼:

下列程式碼不提供環境值和明確值:{ 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 會產生具有動作方法絕對路徑的 URL。 URL 包含指定的 action 名稱和 route 值。

下列程式碼提供來自目前要求的環境值和明確值:{ page = "./Edit, id = 17, }

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

上述程式碼會將編輯 Razor 頁面包含下列頁面指示詞時,將 url 設定為 /Edit/17

@page "{id:int}"

如果 [編輯] 頁面不包含 "{id:int}" 路由範本,url 則為 /Edit?id=17

MVC IUrlHelper 的行為除了此處所述的規則之外,也會新增一層複雜度:

  • IUrlHelper 一律提供目前要求的路由值作為環境值。
  • 除非開發人員覆寫,否則 IUrlHelper.Action 一律會將目前 action 值和 controller 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會將目前的 page 路由值複製為明確值。
  • 除非覆寫,否則 IUrlHelper.Page 一律會覆寫目前 handler 路由值,其中 null 做為明確值。

使用者通常對環境值的行為詳細資料感到驚訝,因為 MVC 似乎沒有遵循自己的規則。 基於歷程記錄和相容性考慮,某些路由值,例如 actioncontrollerpagehandler 有自己的特殊案例行為。

LinkGenerator.GetPathByActionLinkGenerator.GetPathByPage 所提供的對等功能會複製這些 IUrlHelper 的異常,以取得相容性。

URL 產生流程

找到一組候選端點之後,URL 產生演算法:

  • 反復處理端點。
  • 傳回第一個成功的結果。

此程式中的第一個步驟稱為路由值無效。 路由值失效是路由決定應該使用環境值和應忽略哪些路由值的流程。 系統會考慮每個環境值,並結合明確值或忽略。

思考環境值角色的最佳方式是,他們嘗試在一些常見情況下儲存應用程式開發人員輸入。 傳統上,環境值有用的案例與 MVC 相關:

  • 連結至相同控制器中的另一個動作時,不需要指定控制器名稱。
  • 連結至相同區域中的另一個控制器時,不需要指定區功能變數名稱。
  • 連結至相同的動作方法時,不需要指定路由值。
  • 連結至應用程式的另一個部分時,您不想傳遞在應用程式該部分沒有意義的路由值。

對傳回 nullLinkGeneratorIUrlHelper 的呼叫通常是因為無法了解路由值無效所造成。 藉由明確指定更多路由值來針對路由值失效進行疑難排解,以查看這是否可解決問題。

路由值失效的運作方式是假設應用程式的 URL 配置是階層式,而階層是由左至右形成。 請考慮基本控制器路由範本 {controller}/{action}/{id?},以直覺方式了解實際運作方式。 值變更會使所有出現在右邊的路由值失效。 這反映了階層的假設。 如果應用程式有 id 的環境值,則作業會為 controller 指定不同的值:

  • id 不會重複使用,因為 {controller} 位於 {id?} 的左邊。

示範此原則的一些範例:

  • 如果明確值包含 id 的值,則會忽略 id 的環境值。 可以使用 controlleraction 的環境值。
  • 如果明確值包含 action 的值,則會忽略 action 的任何環境值。 可以使用 controller 的環境值。 如果 action 的明確值與 action 的環境值不同,則不會使用 id 值。 如果 action 的明確值與 action 的環境值相同,則可以使用 id 值。
  • 如果明確值包含 controller 的值,則會忽略 controller 的任何環境值。 如果 controller 的明確值與 controller 的環境值不同,則不會使用 actionid 值。 如果 controller 的明確值與 controller 的環境值相同,則可以使用 actionid 值。

透過屬性路由和專用傳統路由的存在,此流程會進一步複雜。 控制器傳統路由,例如 {controller}/{action}/{id?} 使用路由參數指定階層。 針對專用傳統路由屬性路由控制器和 Razor 頁面:

  • 路由值有階層。
  • 它們不會出現在範本中。

在這些情況下,URL 產生會定義必要值概念。 控制器和 Razor 頁面所建立的端點具有指定的必要值,可讓路由值失效運作。

路由值無效演算法詳細資料:

  • 必要的值名稱會與路由參數結合,然後從左至右進行處理。
  • 針對每個參數,會比較環境值和明確值:
    • 如果環境值和明確值相同,流程會繼續。
    • 如果環境值存在且明確值不存在,則產生 URL 時會使用環境值。
    • 如果環境值不存在且明確值存在,請拒絕環境值和所有後續的環境值。
    • 如果環境值和明確值存在,且兩個值不同,請拒絕環境值和所有後續的環境值。

此時,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

路由值不正確問題

從 ASP.NET Core 3.0 開始,在舊 ASP.NET Core 版本中使用的一些 URL 產生配置不適用於 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 參數不會接受為環境值:

  • "default" 路由範本中,culture 路由參數位於 controller 的左邊,因此 controller 的變更不會使 culture無效。
  • "blog" 路由範本中,culture 路由參數會視為 controller 的右邊,其會出現在必要值中。

設定端點中繼資料

下列連結提供設定端點中繼資料的相關資訊:

搭配 RequireHost 路由中的主機比對

RequireHost 會將條件約束套用至需要指定主機的路由。 RequireHost 或 [主機] 參數可以是:

  • 主機:www.domain.com,符合任何連接埠 www.domain.com
  • 具有萬用字元的主機:*.domain.com、比對任何連接埠上的 www.domain.comsubdomain.domain.comwww.subdomain.domain.com
  • 連接埠:*:5000,符合連接埠 5000 與任何主機。
  • 主機和連接埠:www.domain.com:5000*.domain.com:5000,符合主機和連接埠。

可以使用 RequireHost[Host] 來指定多個參數。 條件約束會比對任何參數有效的主機。 例如,[Host("domain.com", "*.domain.com")] 符合 domain.comwww.domain.comsubdomain.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");
    });
}

下列程式碼會使用控制器上的 [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] 屬性同時套用至控制器和動作方法時:

  • 會使用動作上的屬性。
  • 控制器屬性會被忽略。

路由的效能指引

ASP.NET Core 3.0 中已更新大部分路由,以提高效能。

當應用程式發生效能問題時,通常會懷疑路由有問題。 路由的原因是控制器和 Razor 頁面等架構會報告架構在其記錄訊息中花費的時間量。 當控制器報告的時間與要求的總時間之間有顯著差異時:

  • 開發人員會將其應用程式程式碼排除為問題的來源。
  • 通常假設路由是原因。

路由是使用數千個端點測試的效能。 一般應用程式不太可能因為太大而遇到效能問題。 路由效能緩慢最常見的根本原因是自訂中介軟體行為不良。

下列程式碼範例示範縮小延遲來源的基本技巧:

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 1 減去 Time 2 會報告 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 和更早的路由可能無法提供正確的擴充點來支援資料庫存取路由。 例如,IRouteConstraintIActionConstraint 是同步的。 MatcherPolicyEndpointSelectorContext 等擴充點是非同步。

程式庫作者的指引

本節包含程式庫作者建置在路由之上的指引。 這些詳細資料旨在確保應用程式開發人員有良好的使用程式庫和架構來擴充路由的體驗。

定義端點

若要建立使用路由進行 URL 比對的架構,請先定義建置在 UseEndpoints 之上的使用者體驗。

DO 建置在 IEndpointRouteBuilder 之上。 這可讓使用者與其他 ASP.NET Core 功能撰寫架構,而不會造成混淆。 每個 ASP.NET Core 範本都包含路由。 假設使用者有路由存在且熟悉。

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

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

DO 會從實作 IEndpointConventionBuilderMapMyFramework(...) 呼叫傳回密封具體型別。 大部分的架構 Map... 方法都遵循此模式。 IEndpointConventionBuilder 介面:

  • 允許中繼資料的可組合性。
  • 以各種擴充方法為目標。

宣告您自己的型別可讓您將自己的架構特定功能新增至建立器。 可以包裝架構宣告的建立器,並將呼叫轉接給它。

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

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

考慮撰寫您自己的 EndpointDataSourceEndpointDataSource 是宣告和更新端點集合的低層級基本類型。 EndpointDataSource 是控制器和 Razor 頁面所使用的強大 API。

路由測試具有非更新資料來源基本範例

根據預設,請勿嘗試註冊 EndpointDataSource。 要求使用者在 UseEndpoints 中註冊您的架構。 路由的理念是預設不會包含任何項目,而且 UseEndpoints 是註冊端點的位置。

建立路由整合式中介軟體

考慮將中繼資料型別定義為介面。

DO 可讓您在類別和方法上使用中繼資料型別作為屬性。

public interface ICoolMetadata
{
    bool IsCool { get; }
}

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

控制器和 Razor 頁面等架構支援將中繼資料屬性套用至型別和方法。 如果您宣告中繼資料型別:

  • 將它們設為可存取的屬性
  • 大部分的使用者都熟悉套用屬性。

將中繼資料型別宣告為介面會新增另一層彈性:

  • 介面是可組合的。
  • 開發人員可以宣告自己結合多個原則的型別。

DO 可讓您覆寫中繼資料,如下列範例所示:

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() { }
}

遵循這些指導方針的最佳方式是避免定義標記中繼資料

  • 不要只尋找中繼資料型別是否存在。
  • 在中繼資料上定義屬性,並檢查屬性。

中繼資料集合是排序的,並支援依優先順序覆寫。 在控制器的情況下,動作方法上的中繼資料最具體。

DO 讓中介軟體在不使用路由的情況下使用。

app.UseRouting();

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

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

作為此指導方針的範例,請考慮 UseAuthorization 中介軟體。 授權中介軟體可讓您傳入後援原則。 如果指定後援原則,則適用於兩者:

  • 不含指定原則的端點。
  • 不符合端點的要求。

這可讓授權中介軟體在路由內容之外很有用。 授權中介軟體可用於傳統中介軟體程式設計。

偵錯診斷

如需詳細的路由診斷輸出,請將 Logging:LogLevel:Microsoft 設定為 Debug。 在開發環境中,在 appsettings.Development.json 中設定記錄層級:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}