ASP.NET Core 中的路由至控制器動作

作者:Ryan NowakKirk LarkinRick Anderson

注意

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

重要

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

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

ASP.NET Core 控制器使用路由中介軟體來比對內送要求的 URL,並將這些 URL 對應至動作。 路由範本:

  • 在啟動時於 Program.cs 中定義,或是在屬性中定義。
  • 描述 URL 路徑應該如何與動作進行比對。
  • 用來產生連結的 URL。 產生的連結通常會在回應中傳回。

動作可以使用慣例路由屬性路由。 將路由放在控制器或動作上,即可讓其使用屬性路由。 如需詳細資訊,請參閱混合路由

此文件:

  • 說明 MVC 與路由之間的互動:
    • 一般 MVC 應用程式如何使用路由功能。
    • 涵蓋兩者:
      • 慣例路由通常會與控制器和檢視搭配使用。
      • 搭配 REST API 使用的屬性路由。 如果您主要想要路由傳送 REST API,請跳至 REST API 的屬性路由一節。
    • 如需進階路由詳細資料,請參閱路由
  • 是指稱為端點路由的預設路由系統。 您可以使用控制器搭配舊版路由,以達到相容性目的。 如需指示,請參閱 2.2-3.0 移轉指南

設定慣例路由

ASP.NET Core MVC 範本會產生類似下列的慣例路由程式碼:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

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

app.Run();

MapControllerRoute 是用來建立單一路由。 單一路由命名為 default route。 大部分具有控制器和檢視的應用程式都會使用類似 default 路由的路由範本。 REST API 應該使用屬性路由

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

路由範本 "{controller=Home}/{action=Index}/{id?}"

  • 符合 URL 路徑,例如 /Products/Details/5

  • 您可以將路徑語彙基元化來擷取路由值 { controller = Products, action = Details, id = 5 }。 如果應用程式具有名為 ProductsController 的控制器和 Details 動作,擷取路由值就會導致相符:

    public class ProductsController : Controller
    {
        public IActionResult Details(int id)
        {
            return ControllerContext.MyDisplayRouteInfo(id);
        }
    }
    

    MyDisplayRouteInfo 是由 Rick.Docs.Samples.RouteInfo NuGet 套件提供,並顯示路由資訊。

  • /Products/Details/5 模型會繫結 id = 5 的值,將 id 參數設定為 5。 如需詳細資料,請參閱模型繫結

  • {controller=Home}Home 定義為預設的 controller

  • {action=Index}Index 定義為預設的 action

  • {id?} 中的 ? 字元會將 id 定義為選擇性。

    • 預設和選擇性路由參數不一定要全部出現在 URL 路徑中才算相符。 如需路由範本語法的詳細描述,請參閱路由範本參考
  • 符合 URL 路徑 /

  • 產生路由值 { controller = Home, action = Index }

controlleraction 的值會使用預設值。 id 不會產生值,因為 URL 路徑中沒有對應的區段。 / 僅在有 HomeControllerIndex 動作時才會相符:

public class HomeController : Controller
{
    public IActionResult Index() { ... }
}

使用上述控制器定義及路由範本,就會對下列 URL 路徑執行 HomeController.Index 動作:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /

URL 路徑 / 會使用路由範本預設 Home 控制器和 Index 動作。 URL 路徑 /Home 會使用路由範本預設 Index 動作。

MapDefaultControllerRoute 方法很方便:

app.MapDefaultControllerRoute();

取代:

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

重要

路由是使用 UseRoutingUseEndpoints 中介軟體進行設定。 若要使用控制器:

應用程式通常不需要呼叫 UseRoutingUseEndpointsWebApplicationBuilder 設定中介軟體管線,此管線會包裝使用 UseRoutingUseEndpointsProgram.cs 中新增的中介軟體。 如需詳細資訊,請參閱 ASP.NET Core 中的路線規劃

慣例路由

慣例路由會與控制器和檢視搭配使用。 default 路由:

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

上述為「慣例路由」的範例。 其稱為「慣例路由」,因為其會建立 URL 路徑的「慣例」

  • 第一個路徑區段 {controller=Home} 對應至控制器名稱。
  • 第二個區段 {action=Index} 會對應至動作名稱。
  • 第三個區段 {id?} 用於選擇性的 id{id?} 中的 ? 會使其成為選擇性的。 id 是用來對應至模型實體。

使用此 default 路由,URL 路徑:

  • /Products/List 對應至 ProductsController.List 動作。
  • /Blog/Article/17 對應至 BlogController.Article,且通常模型會將 id 參數繫結至 17。

此對應:

  • 以控制器和動作名稱為基礎。
  • 不是以命名空間、來源檔案位置或方法參數為基礎。

使用慣例路由搭配預設路由可建立應用程式,而不必針對每個動作想出新的 URL 模式。 針對具有 CRUD 樣式動作的應用程式,在控制器之間具有 URL 的一致性:

  • 協助簡化程式碼。
  • 讓 UI 更容易預測。

警告

路由範本會將上述程式碼中的 id 定義為選擇性。 動作可以執行,而不需提供做為 URL 一部分的選擇性識別碼。 一般而言,當從 URL 省略時 id

  • id 是由模型繫結設定為 0
  • 在符合 id == 0 的資料庫中找不到任何實體。

屬性路由可提供更細微的控制,讓某些動作需要此識別碼,而其他動作則不需要。 依照慣例,本文件將會包含可能出現在正確使用中的選擇性參數 (例如 id)。

大部分應用程式都應該選擇基本的描述性路由傳送配置,讓 URL 可讀且有意義。 預設慣例路由 {controller=Home}/{action=Index}/{id?}

  • 支援基本的描述性路由配置。
  • 適合作為 UI 型應用程式的起點。
  • 這是許多 Web UI 應用程式唯一需要的路由範本。 對於較大的 Web UI 應用程式,經常只需要使用區域的另一個路由。

MapControllerRouteMapAreaRoute

  • 根據叫用端點的順序,自動將訂單值指派給其端點。

ASP.NET Core 中的端點路由:

  • 沒有路由的概念。
  • 不會提供執行擴充性的順序保證,所有端點都會一次處理。

啟用記錄以查看內建路由實作 (例如 Route) 如何符合要求。

本文件稍後會說明屬性路由

多個慣例路由

您可以對 MapControllerRouteMapAreaControllerRoute 新增更多呼叫,來設定多個慣例路由。 這樣做可讓您定義多個慣例,或新增特定動作專用的慣例路由,例如:

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

上述程式碼中的 blog 路由是專用的慣例路由。 其稱為專用的慣例路由,因為:

因為 controlleraction 不會出現在路由範本 "blog/{*article}" 中做為參數:

  • 其只能有預設值 { controller = "Blog", action = "Article" }
  • 此路由一律會對應至動作 BlogController.Article

/Blog/Blog/Article/Blog/{any-string} 是唯一符合部落格路由的 URL 路徑。

上述範例:

  • blog 路由的比對優先順序高於 default 路由,因為其會先新增。
  • 這是 Slug 樣式路由的範例,其中通常會有文章名稱做為 URL 的一部分。

警告

在 ASP.NET Core 中,路由不會:

  • 定義稱為路由的概念。 UseRouting 會將路由比對新增至中介軟體管線。 UseRouting 中介軟體會查看應用程式中定義的端點集合,並根據要求選取最佳端點比對。
  • 提供 IRouteConstraintIActionConstraint 等擴充性執行順序的保證。

如需路由的參考資料,請參閱路由

慣例路由順序

慣例路由只會比對應用程式所定義的動作和控制器組合。 這目的在於簡化慣例路由重疊的情況。 使用 MapControllerRouteMapDefaultControllerRouteMapAreaControllerRoute 新增路由,並根據叫用的順序,自動將訂單值指派給其端點。 來自稍早所出現路由的相符項目具有較高的優先順序。 慣例路由與順序息息相關。 一般而言,具有區域的路由應該放在前面,因為這些路由比沒有區域的路由更明確。 具有 catch-all 路由參數的專用慣例路由 (例如 {*article}) 可能會使路由變得太窮盡,這表示其符合您打算與其他路由比對的 URL。 將窮盡路由放在路由表中,以防止窮盡的相符項目。

警告

由於路由中的錯誤 (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.

解決語意模糊的動作

當透過路由比對兩個端點時,路由必須執行下列其中一項:

  • 選擇最佳候選項目。
  • 擲回例外狀況。

例如:

public class Products33Controller : Controller
{
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpPost]
    public IActionResult Edit(int id, Product product)
    {
        return ControllerContext.MyDisplayRouteInfo(id, product.name);
    }
}

上述控制器會定義兩個符合的動作:

  • URL 路徑 /Products33/Edit/17
  • 路由資料 { controller = Products33, action = Edit, id = 17 }

這是 MVC 控制器的典型模式:

  • Edit(int) 顯示要編輯產品的表單。
  • Edit(int, Product) 會處理張貼的表單。

若要解析正確的路由:

  • 當要求為 HTTP POST 時,會選取 Edit(int, Product)
  • Http 指令動詞是任何其他動作時,就會選取 Edit(int)Edit(int) 通常會透過 GET 進行呼叫。

會提供 HttpPostAttribute[HttpPost] 來路由,以便根據要求的 HTTP 方法進行選擇。 HttpPostAttribute 會使 Edit(int, Product) 成為比 Edit(int) 更佳的相符項目。

請務必了解屬性的角色,例如 HttpPostAttribute。 其他 Http 指令動詞會定義類似的屬性。 在慣例路由中,當動作是顯示表單、提交表單工作流程的一部分時,通常會使用相同的動作名稱。 例如,請參閱檢查兩個編輯動作方法

如果路由無法選擇最佳候選項目,則會擲回 AmbiguousMatchException,並列出多個相符的端點。

慣例路由名稱

下列範例中的字串 "blog""default" 是慣例路由名稱:

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

路由名稱會為路由提供邏輯名稱。 具名路由可用於產生 URL。 當路由排序可能使產生 URL 作業變得複雜時,使用具名路由可大幅簡化產生 URL 作業。 在整個應用程式內路由名稱必須是唯一的。

路由名稱:

  • 不會影響要求的 URL 比對或處理。
  • 僅用於產生 URL。

路由名稱概念是在路由中以 IEndpointNameMetadata 表示。 字詞路由名稱端點名稱

  • 可互換。
  • 文件與程式碼中會使用哪一個,取決於所描述的 API。

REST API 的屬性路由

REST API 應該使用屬性路由傳送來將應用程式功能模型建構為作業由 HTTP 指令動詞代表的資源集合。

屬性路由使用一組屬性,將動作直接對應至路由範本。 下列程式碼為 REST API 的一般用法,並用於下一個範例:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

在上述程式碼中,會呼叫 MapControllers 來對應屬性路由控制器。

在以下範例中:

  • HomeController 會比對一組類似預設慣例路由 {controller=Home}/{action=Index}/{id?} 相符的 URL。
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

HomeController.Index 動作會針對任何 URL 路徑 //Home/Home/Index/Home/Index/3 執行。

此範例醒目提示屬性路由與慣例路由之間的主要程式設計差異。 屬性路由需要更多輸入來指定路由。 慣例的預設路由會更簡潔地處理路由。 不過,屬性路由允許 (並需要) 精確地控制套用至每個動作的路由範本。

使用屬性路由時,除非使用語彙基元取代,否則控制器和動作名稱不會發揮比對動作的作用。 下列範例會比對與上一個範例相同的 URL:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

下列程式碼會使用 actioncontroller 的語彙基元取代 :

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

下列程式碼適用於 [Route("[controller]/[action]")] 控制器:

[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]
    [Route("/Home")]
    [Route("~/Home/Index")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

在上述程式碼中,Index 方法範本前面必須加上 /~/ 以路由範本。 套用至開頭為 /~/ 之動作的路由範本,無法與套用至控制器的路由範本合併。

如需路由範本選取項目的相關資訊,請參閱路由範本優先順序

保留的路由名稱

使用控制器或 Razor Pages 時,下列關鍵字是保留路由參數名稱:

  • action
  • area
  • controller
  • handler
  • page

使用 page 做為具有屬性路由的路由參數是常見的錯誤。 這麼做會導致產生 URL 的行為不一致且令人困惑。

public class MyDemo2Controller : Controller
{
    [Route("/articles/{page}")]
    public IActionResult ListArticles(int page)
    {
        return ControllerContext.MyDisplayRouteInfo(page);
    }
}

產生 URL 會使用特殊參數名稱來判斷 URL 產生作業是參考 Razor Page 還是 Controller。

下列關鍵字會保留於 Razor 檢視或 Razor Page 的內容中:

  • page
  • using
  • namespace
  • inject
  • section
  • inherits
  • model
  • addTagHelper
  • removeTagHelper

這些關鍵字不應該用於連結世代、模型繫結參數或最上層屬性。

Http 指令動詞範本

ASP.NET Core 具有下列 Http 指令動詞範本:

路由範本

ASP.NET Core 具有下列路由範本:

使用 Http 指令動詞屬性的屬性路由

請考慮下列控制器:

[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中:

  • 每個動作都包含 [HttpGet] 屬性,其只會限制與 HTTP GET 要求的比對。
  • GetProduct 動作包含 "{id}" 範本,因此 id 會附加至控制器上的 "api/[controller]" 範本。 方法範本為 "api/[controller]/{id}"。 因此,此動作只會比對表單 /api/test2/xyz/api/test2/123/api/test2/{any string} 等等的 GET 要求。
    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }
    
  • GetIntProduct 動作包含 "int/{id:int}" 範本。 範本的 :int 部分會將 id 路由值限制為可轉換成整數的字串。 對 /api/test2/int/abc 的 GET 要求:
    • 不符合此動作。
    • 傳回 404 找不到錯誤。
      [HttpGet("int/{id:int}")] // GET /api/test2/int/3
      public IActionResult GetIntProduct(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      
  • GetInt2Product 動作在範本中包含 {id},但不會將 id 限制為可轉換成整數的值。 對 /api/test2/int2/abc 的 GET 要求:
    • 符合此路由。
    • 模型繫結無法將 abc 轉換成整數。 方法的 id 參數是整數。
    • 傳回 400 不正確的要求,因為模型繫結無法將 abc 轉換成整數。
      [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
      public IActionResult GetInt2Product(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      

屬性路由可以使用 HttpMethodAttribute 屬性,例如 HttpPostAttributeHttpPutAttributeHttpDeleteAttribute。 所有 Http 指令動詞屬性都接受路由範本。 下列範例顯示兩個符合相同路由範本的動作:

[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}

使用 URL 路徑 /products3

  • Http 指令動詞GET 時,MyProductsController.ListProducts 動作會執行。
  • Http 指令動詞POST 時,MyProductsController.CreateProduct 動作會執行。

建置 REST API 時,您很少需要在動作方法上使用 [Route(...)],因為動作會接受所有 HTTP 方法。 最好是使用更明確的 Http 指令動詞屬性,以精確地指定 API 的支援項目。 REST API 的用戶端必須知道哪些路徑和 HTTP 動詞命令對應至特定邏輯作業。

REST API 應該使用屬性路由傳送來將應用程式功能模型建構為作業由 HTTP 指令動詞代表的資源集合。 這表示相同邏輯資源上的許多作業 (例如,GET、POST) 都會使用相同的 URL。 屬性路由提供仔細設計 API 公用端點配置所需的控制層級。

由於屬性路由會套用至特定動作,因此輕鬆就能將參數設為路由範本定義的必要部分。 在下列範例中,需要 id 做為 URL 路徑的一部分:

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Products2ApiController.GetProduct(int) 動作:

  • 使用 URL 路徑執行,例如 /products2/3
  • 不會使用 URL 路徑 /products2 執行。

[Consumes] 屬性可讓動作限制支援的要求內容類型。 如需詳細資訊,請參閱使用 Consumes 屬性定義支援的要求內容類型

如需路由範本和相關選項的完整描述,請參閱路由

如需 [ApiController] 的詳細資訊,請參閱 ApiController 屬性

路由名稱

下列程式碼會定義 Products_List 的路由名稱:

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

您可以使用路由名稱根據特定路由來產生 URL。 路由名稱:

  • 不會影響路由的 URL 比對行為。
  • 僅用於產生 URL。

在整個應用程式內路由名稱必須是唯一的。

上述程式碼與慣例預設路由相反,只會將 id 參數定義為選擇性 ({id?})。 能夠精確指定 API 有些優點,像是允許將 /products/products/5 分派至不同的動作。

合併屬性路由

為了避免屬性路由過於重複,控制器上的路由屬性可與個別動作上的路由屬性合併。 控制器上定義的任何路由範本都會附加到動作上的路由範本之前。 將路由屬性放在控制器上,即可讓控制器中的所有動作使用屬性路由。

[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在前述範例中:

  • URL 路徑 /products 可以比對 ProductsApi.ListProducts
  • URL 路徑 /products/5 可以比對 ProductsApi.GetProduct(int)

由於這兩種動作是以 [HttpGet] 屬性標示,因此只會符合 HTTP GET

套用至開頭為 /~/ 之動作的路由範本,無法與套用至控制器的路由範本合併。 下列範例會比對一組類似於預設路由的 URL 路徑。

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    [Route("/")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("About")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

下表說明上述程式碼中的 [Route] 屬性:

屬性 結合 [Route("Home")] 定義路由範本
[Route("")] Yes "Home"
[Route("Index")] .是 "Home/Index"
[Route("/")] ""
[Route("About")] Yes "Home/About"

屬性路由順序

路由會建置樹狀結構,並同時符合所有端點:

  • 路由項目的行為就像是放置在理想順序中一樣。
  • 最特定的路由有機會在較一般路由之前執行。

例如,類似 blog/search/{topic} 的屬性路由比 blog/{*article} 等屬性路由更具體。 blog/search/{topic} 路由預設具有較高的優先順序,因為其更具體。 使用慣例路由,開發人員會負責依想要的順序來排列路由。

屬性路由可以使用 Order 屬性來設定訂單。 所有架構提供的路由屬性都包含 Order。 路由會依 Order 屬性的遞增排序來處理。 預設順序為 0。 使用 Order = -1 設定的路由會在未設定順序的路由之前執行。 使用 Order = 1 設定的路由會在預設路由排序之後執行。

避免依賴 Order。 如果應用程式的 URL 空間需要明確的順序值才能正確地路由,則同樣也可能會使用戶端混淆。 一般而言,屬性路由會透過 URL 比對來選取正確的路由。 如果用於 URL 產生的預設順序無效,使用路由名稱做為覆寫通常會比套用 Order 屬性更簡單。

請考慮下列兩個控制器,這兩個控制器都會定義比對 /home 的路由:

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

使用上述程式碼要求 /home 會擲回類似下列的例外狀況:

AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex

Order 新增至其中一個路由屬性會解決語意模糊:

[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}

使用上述程式碼,/home 會執行 HomeController.Index 端點。 若要取得 MyDemoController.MyIndex,請要求 /home/MyIndex注意

  • 上述程式碼是範例或路由設計不佳。 其用來說明 Order 屬性。
  • 屬性 Order 只會解決語意模糊,並無法比對該範本。 最好移除 [Route("Home")] 範本。

如需使用 Razor Pages 的路由順序資訊,請參閱 Razor Pages 路由和應用程式慣例:路由順序

在某些情況下,HTTP 500 錯誤會以語意模糊的路由傳回。 使用記錄來查看導致 AmbiguousMatchException 的端點。

路由範本中的語彙基元取代 [controller]、[action]、[area]

為了方便起見,屬性路由支援以方括號 ([]) 括住語彙基元的「語彙基元取代」。 語彙基元 [action][area][controller] 會分別以定義路由之動作的動作名稱值、區域名稱值和控制器名稱值來取代:

[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中:

[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
  • 符合 /Products0/List
[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
  • 符合 /Products0/Edit/{id}

語彙基元取代會在建立屬性路由的最後一個步驟發生。 上述範例的行為與下列程式碼相同:

public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

如果您要以英文以外的語言閱讀本文,則若您想要以母語查看程式碼註解,請在此 GitHub 討論問題中告訴我們。

屬性路由也可以透過繼承來合併。 這與語彙基元取代結合會很強大。 語彙基元取代也適用於屬性路由所定義的路由名稱。 [Route("[controller]/[action]", Name="[controller]_[action]")] 會針對每個動作產生唯一的路由名稱:

[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

若要比對常值語彙基元取代分隔符號 [],請重複字元 ([[]]) 來將它逸出。

使用參數轉換程式自訂語彙基元取代

可以使用參數轉換程式自訂語彙基元取代。 參數轉換程式會實作 IOutboundParameterTransformer 並轉換參數值。 例如,自訂 SlugifyParameterTransformer 參數轉換器會將 SubscriptionManagement 路由值變更為 subscription-management

using System.Text.RegularExpressions;

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();
    }
}

RouteTokenTransformerConvention 是一個應用程式模型慣例,它會:

  • 將參數轉換程式套用到應用程式中的所有屬性路由。
  • 在取代屬性路由語彙基元值時自訂它們。
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")]
    public IActionResult ListAll()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

上述 ListAll 方法符合 /subscription-management/list-all

RouteTokenTransformerConvention 會註冊為選項:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(new RouteTokenTransformerConvention(
                                 new SlugifyParameterTransformer()));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

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

app.Run();

如需 Slug 的定義,請參閱 Slug 上的 MDN Web 文件

警告

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

多個屬性路由

屬性路由支援定義多個路由來達到相同的動作。 最常見的用法是模擬「預設慣例路由」的行為,如下列範例所示:

[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

將多個路由屬性放在控制器上,表示這些屬性會各自與動作方法上的每個路由屬性合併:

[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

所有 Http 指令動詞路由條件約束都會實作 IActionConstraint

當實作的 IActionConstraint 多個路由屬性放在動作上時:

  • 每個動作條件約束都會結合套用至控制器的路由範本。
[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

在動作上使用多個路由看似很有用且強大,但最好保持基本且妥善定義的應用程式 URL 空間。 只有在必要時,才在動作上使用多個路由;例如,為了支援現有的用戶端。

指定屬性路由的選擇性參數、預設值和條件約束

屬性路由支援使用與慣例路由相同的內嵌語法,來指定選擇性參數、預設值和條件約束。

public class Products14Controller : Controller
{
    [HttpPost("product14/{id:int}")]
    public IActionResult ShowProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中,[HttpPost("product14/{id:int}")] 會套用路由條件約束。 Products14Controller.ShowProduct 動作只會由 URL 路徑比對,例如 /product14/3。 路由範本部分 {id:int} 會將該區段限制為只有整數。

如需路由範本語法的詳細描述,請參閱路由範本參考

使用 IRouteTemplateProvider 的自訂路由屬性

所有路由屬性都會實作 IRouteTemplateProvider。 ASP.NET Core 執行階段:

  • 在應用程式啟動時,尋找控制器類別和動作方法的屬性。
  • 使用實作 IRouteTemplateProvider 的屬性來建置初始路由集。

實作 IRouteTemplateProvider 以定義自訂路由屬性。 每個 IRouteTemplateProvider 都可讓您定義具有自訂路由範本、順序和名稱的單一路由:

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";
    public int? Order => 2;
    public string Name { get; set; } = string.Empty;
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
    // GET /api/MyTestApi
    [HttpGet]
    public IActionResult Get()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

上述 Get 方法會傳回 Order = 2, Template = api/MyTestApi

使用應用程式模型自訂屬性路由

應用程式模型:

  • 這是在 Program.cs 中啟動時所建立的物件模型。
  • 包含 ASP.NET Core 用來路由及執行應用程式中動作的所有中繼資料。

應用程式模型包含從路由屬性收集的所有資料。 IRouteTemplateProvider 實作會提供來自路由屬性的資料。 慣例:

  • 您可以撰寫來修改應用程式模型,以自訂路由的行為。
  • 在應用程式啟動時讀取。

本節說明使用應用程式模型來自訂路由的基本範例。 下列程式碼會讓路由大致與專案的資料夾結構對齊。

public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector =>
                                                selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            return;
        }

        var namespc = controller.ControllerType.Namespace;
        if (namespc == null)
            return;
        var template = new StringBuilder();
        template.Append(namespc, _baseNamespace.Length + 1,
                        namespc.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]/[action]/{id?}");

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString()
            };
        }
    }
}

下列程式碼可防止 namespace 慣例套用至路由屬性的控制器:

public void Apply(ControllerModel controller)
{
    var hasRouteAttributes = controller.Selectors.Any(selector =>
                                            selector.AttributeRouteModel != null);
    if (hasRouteAttributes)
    {
        return;
    }

例如,下列控制器不使用 NamespaceRoutingConvention

[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
    // /managers/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        return Content($"Index- template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

NamespaceRoutingConvention.Apply 方法:

  • 如果控制器路由屬性,則不會執行任何動作。
  • 根據 namespace 設定控制器範本,並移除基底 namespace

您可以在 Program.cs 中套用 NamespaceRoutingConvention

using My.Application.Controllers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(
     new NamespaceRoutingConvention(typeof(HomeController).Namespace!));
});

var app = builder.Build();

例如,請考量下列控制器:

using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
    public class UsersController : Controller
    {
        // GET /admin/controllers/users/index
        public IActionResult Index()
        {
            var fullname = typeof(UsersController).FullName;
            var template = 
                ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
            var path = Request.Path.Value;

            return Content($"Path: {path} fullname: {fullname}  template:{template}");
        }

        public IActionResult List(int? id)
        {
            var path = Request.Path.Value;
            return Content($"Path: {path} ID:{id}");
        }
    }
}

在上述程式碼中:

  • 基底 namespaceMy.Application
  • 上述控制器的完整名稱為 My.Application.Admin.Controllers.UsersController
  • NamespaceRoutingConvention 會將控制器範本設定為 Admin/Controllers/Users/[action]/{id?

NamespaceRoutingConvention 也可以套用為控制器上的屬性:

[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
    // /admin/controllers/test/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        var actionname = ControllerContext.ActionDescriptor.ActionName;
        return Content($"Action- {actionname} template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

混合路由:屬性路由與慣例路由

ASP.NET Core 應用程式可以混用慣例路由與屬性路由。 控制器通常會使用慣例路由來提供 HTML 頁面給瀏覽器,並使用屬性路由來提供 REST API。

動作可以使用慣例路由或屬性路由。 將路由放在控制器或動作上,即可讓它使用屬性路由。 定義屬性路由的動作無法透過慣例路由到達,反之亦然。 控制器上的任何路由屬性可讓控制器中的所有動作使用屬性路由。

屬性路由和慣例路由使用相同的路由引擎。

具有特殊字元的路由

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

[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 產生功能,來產生動作的 URL 連結。 產生 URL 可排除硬式編碼 URL,讓程式碼更穩定且更容易維護。 本節著重於 MVC 所提供的 URL 產生功能,並只會涵蓋 URL 產生運作方式的基本概念。 如需 URL 產生的詳細描述,請參閱路由

IUrlHelper 介面是 MVC 與用於產生 URL 的路由之間的基礎結構元素。 透過控制器、檢視和檢視元件中 Url 屬性,來尋找可用的 IUrlHelper 執行個體。

在下列範例中,會透過 Controller.Url 屬性使用 IUrlHelper 介面來產生另一個動作的 URL。

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

如果應用程式使用預設慣例路由,url 變數的值會是 URL 路徑字串 /UrlGeneration/Destination。 此 URL 路徑是結合下列方式加以建立:

  • 目前要求的路由值,稱為環境值
  • 將值傳遞給 Url.Action 並將這些值取代為路由範本:
ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

路由範本中每個路由參數的值都會以相符名稱的值和環境值所取代。 沒有值的路由參數可以:

  • 如果有預設值,請使用預設值。
  • 如果這是選擇性的,請略過。 例如,來自路由範本 {controller}/{action}/{id?}id

如果任何必要的路由參數沒有對應的值,URL 產生會失敗。 如果某個路由的 URL 產生失敗,則會嘗試下一個路由,直到嘗試所有路由或找到相符項目為止。

上述 Url.Action 範例假設使用慣例路由。 雖然概念不同,但產生 URL 的運作方式與屬性路由類似。 使用慣例路由:

  • 路由值可用來展開範本。
  • controlleraction 的路由值通常會出現在該範本中。 這行得通,因為路由所比對的 URL 會遵守慣例。

下列範例使用屬性路由:

public class UrlGenerationAttrController : Controller
{
    [HttpGet("custom")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination()
    {
       return ControllerContext.MyDisplayRouteInfo();
    }
}

上述程式碼中的 Source 動作會產生 custom/url/to/destination

已在 ASP.NET Core 3.0 中新增 LinkGenerator,做為 IUrlHelper 的替代方案。 LinkGenerator 提供類似但更具彈性的功能。 IUrlHelper 上的每個方法在 LinkGenerator 上也有對應的方法系列。

由動作名稱產生 URL

Url.ActionLinkGenerator.GetPathByAction 和所有相關多載全都設計成透過指定控制器名稱和動作名稱來產生目標端點。

使用 Url.Action 時,執行階段會提供 controlleraction 的目前路由值:

  • controlleraction 的值同時屬於環境值和值。 方法 Url.Action 一律會使用 actioncontroller 的目前值,且會產生路由至目前動作的 URL 路徑。

路由會嘗試使用環境值中的值,來填入在產生 URL 時所未提供的資訊。 請考慮使用 {a}/{b}/{c}/{d} 之類的路由搭配 { a = Alice, b = Bob, c = Carol, d = David } 環境值:

  • 路由有足夠的資訊來產生 URL,而不需要任何額外的值。
  • 路由有足夠的資訊,因為所有路由參數都有一個值。

如果已新增值 { d = Donovan }

  • 會忽略 { d = David } 值。
  • 產生的 URL 路徑為 Alice/Bob/Carol/Donovan

警告:URL 路徑是階層式。 在上述範例中,如果已新增值 { c = Cheryl }

  • 會忽略 { c = Carol, d = David } 這兩個值。
  • 不再有 d 的值,且產生 URL 失敗。
  • 必須指定 cd 的所需值,才能產生 URL。

您可能預期會使用預設路由 {controller}/{action}/{id?} 來解決此問題。 此問題在實務上很少見,因為 Url.Action 一律會明確指定 controlleraction 值。

數個 Url.Action 多載會接受一個路由值物件,來為 controlleraction 以外的路由參數提供值。 路由值物件經常與 id 搭配使用。 例如: Url.Action("Buy", "Products", new { id = 17 }) 。 路由值物件:

  • 依照慣例,通常是匿名型別的物件。
  • 可以是 IDictionary<>POCO)。

不符合路由參數的任何額外路由值都會放在查詢字串中。

public IActionResult Index()
{
    var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
    return Content(url!);
}

上述程式碼會產生 /Products/Buy/17?color=red

下列程式碼會產生絕對 URL:

public IActionResult Index2()
{
    var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
    // Returns https://localhost:5001/Products/Buy/17
    return Content(url!);
}

若要建立絕對 URL,請使用下列其中一項:

依路由產生 URL

上述程式碼示範如何藉由傳入控制器和動作名稱來產生 URL。 IUrlHelper 也提供方法的 Url.RouteUrl 系列。 這些方法類似於 Url.Action,但不會將 actioncontroller 的目前值複製到路由值。 最常見的 Url.RouteUrl 用法:

  • 指定要產生 URL 的路由名稱。
  • 通常不會指定控制器或動作名稱。
public class UrlGeneration2Controller : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

下列 Razor 檔案會產生 Destination_Route 的 HTML 連結:

<h1>Test Links</h1>

<ul>
    <li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>

在 HTML 和 Razor 中產生 URL

IHtmlHelper 提供 Html.BeginFormHtml.ActionLinkHtmlHelper 方法,分別產生 <form><a> 元素。 這些方法使用 Url.Action 方法來產生 URL,並接受類似的引數。 HtmlHelper 的成對 Url.RouteUrlHtml.BeginRouteFormHtml.RouteLink,這兩者的功能很類似。

TagHelper 透過 form TagHelper 和 <a> TagHelper 產生 URL。 這兩者使用 IUrlHelper 進行實作。 如需詳細資訊,請參閱表單中的標籤協助程式

在檢視中,可透過 Url 屬性使用 IUrlHelper 來產生上述未涵蓋的任何特定 URL。

在動作結果中產生 URL

以上顯示的範例在控制器中使用 IUrlHelper。 控制器中最常見的用法是產生 URL 做為動作結果的一部分。

ControllerBaseController 基底類別提供便利的方法讓動作結果可參考其他動作。 一個典型的用法是在接受使用者輸入之後重新導向:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        ViewData["Message"] = $"Successful edit of customer {id}";
        return RedirectToAction("Index");
    }
    return View(customer);
}

動作結果的 Factory 方法 (例如 RedirectToActionCreatedAtAction) 遵循類似於 IUrlHelper 上方法的模式。

專用慣例路由的特殊案例

慣例路由可使用一種特殊路由定義,稱為專用慣例路由。 在下列範例中,名為 blog 的路由是專用慣例路由:

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

使用上述路由定義,Url.Action("Index", "Home") 會使用 default 路由來產生 URL 路徑 /,但原因為何? 您可能會猜想路由值 { controller = Home, action = Index } 便足以使用 blog 來產生 URL,且結果會是 /blog?action=Index&controller=Home

專用慣例路由依賴沒有對應路由參數預設值的特殊行為,以防止用於 URL 產生的路由變得太窮盡。 在本例中,預設值為 { controller = Blog, action = Article }controlleraction 都不會顯示為路由參數。 當路由執行 URL 產生時,所提供的值必須符合預設值。 使用 blog 產生 URL 會失敗,因為值 { controller = Home, action = Index } 不符合 { controller = Blog, action = Article }。 路由會接著切換並嘗試 default,此時會成功。

領域

區域是一項 MVC 功能,用來將相關功能組織成群組以進行區隔:

  • 控制器動作的路由命名空間。
  • 檢視的資料夾結構。

使用區域可讓應用程式具有多個同名的控制器,只要這些控制器具有不同的區域即可。 使用區域可建立用於路由的階層,方法是將另一個路由參數 area 新增至 controlleraction。 本節討論路由如何與區域互動。 如需如何搭配檢視使用區域的詳細資訊,請參閱區域

下列範例會設定 MVC 使用預設慣例路由,並為名為 Blogarea 設定 area 路由:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{    
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute("blog_route", "Blog",
        "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");

app.Run();

在上述程式碼中,會呼叫 MapAreaControllerRoute 以建立 "blog_route"。 第二個參數 "Blog" 是區域名稱。

當符合 /Manage/Users/AddUser 等 URL 路徑時,"blog_route" 路由會產生路由值 { area = Blog, controller = Users, action = AddUser }area 路由值是由 area 的預設值所產生。 MapAreaControllerRoute 所建立的路由相當於下列項目:

app.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");

MapAreaControllerRoute 會針對使用所提供之區域名稱 (在本例中為 Blog) 的 area,使用預設值和條件約束來建立路由。 預設值可確保路由一律會產生 { area = Blog, ... },而條件約束需要 { area = Blog, ... } 值以產生 URL。

慣例路由與順序息息相關。 一般而言,具有區域的路由應該放在前面,因為這些路由比沒有區域的路由更明確。

使用上述範例,路由值 { area = Blog, controller = Users, action = AddUser } 符合下列動作:

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}

[Area] 屬性代表控制器做為區域的一部分。 此控制器位於 Blog 區域中。 不含 [Area] 屬性的控制器不是任何區域的成員,因此當路由提供 area 路由值時不會符合。 在下列範例中,只有列出的第一個控制器可能符合路由值 { area = Blog, controller = Users, action = AddUser }

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        // GET /zebra/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        // GET /users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }
    }
}

這裡會顯示每個控制器的命名空間,以取得完整性。 如果上述控制器使用相同的命名空間,則會產生編譯器錯誤。 類別命名空間不會影響 MVC 的路由。

前兩個控制器是區域的成員,只有在 area 路由值提供其各自的區域名稱時才會符合。 第三個控制器不是任何區域的成員,只有路由未提供任何值給 area 時才會符合。

在「未符合任何值」的情況下,缺少 area 值相當於 area 的值為 Null 或空字串。

執行區域中的動作時,area 的路由值會作為路由用於 URL 產生的「環境值」。 這表示區域預設會以「黏性」方式來處理 URL 產生,如下列範例所示。

app.MapAreaControllerRoute(name: "duck_route",
                                     areaName: "Duck",
                                     pattern: "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute(name: "default",
                             pattern: "Manage/{controller=Home}/{action=Index}/{id?}");
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
    [Area("Duck")]
    public class UsersController : Controller
    {
        // GET /Manage/users/GenerateURLInArea
        public IActionResult GenerateURLInArea()
        {
            // Uses the 'ambient' value of area.
            var url = Url.Action("Index", "Home");
            // Returns /Manage/Home/Index
            return Content(url);
        }

        // GET /Manage/users/GenerateURLOutsideOfArea
        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area.
            var url = Url.Action("Index", "Home", new { area = "" });
            // Returns /Manage
            return Content(url);
        }
    }
}

下列程式碼會產生 /Zebra/Users/AddUser 的 URL:

public class HomeController : Controller
{
    public IActionResult About()
    {
        var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
        return Content($"URL: {url}");
    }

動作定義

控制器上的公用方法 (不包括以 NonAction 屬性裝飾的項目) 就是動作。

範例指令碼

偵錯診斷

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

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

ASP.NET Core 控制器使用路由中介軟體來比對內送要求的 URL,並將這些 URL 對應至動作。 路由範本:

  • 在啟動程式碼或屬性中定義。
  • 描述 URL 路徑應該如何與動作進行比對。
  • 用來產生連結的 URL。 產生的連結通常會在回應中傳回。

動作可以使用慣例路由屬性路由。 將路由放在控制器或動作上,即可讓其使用屬性路由。 如需詳細資訊,請參閱混合路由

此文件:

  • 說明 MVC 與路由之間的互動:
    • 一般 MVC 應用程式如何使用路由功能。
    • 涵蓋兩者:
      • 慣例路由通常會與控制器和檢視搭配使用。
      • 搭配 REST API 使用的屬性路由。 如果您主要想要路由傳送 REST API,請跳至 REST API 的屬性路由一節。
    • 如需進階路由詳細資料,請參閱路由
  • 是指 ASP.NET Core 3.0 中新增的預設路由系統,稱為端點路由。 您可以使用控制器搭配舊版路由,以達到相容性目的。 如需指示,請參閱 2.2-3.0 移轉指南。 如需舊版路由系統的參考資料,請參閱本文件的 2.2 版

設定慣例路由

使用Startup.Configure慣例路由時, 通常會有類似下列的程式碼:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

UseEndpoints 呼叫內,MapControllerRoute 會用來建立單一路由。 單一路由命名為 default route。 大部分具有控制器和檢視的應用程式都會使用類似 default 路由的路由範本。 REST API 應該使用屬性路由

路由範本 "{controller=Home}/{action=Index}/{id?}"

  • 符合 URL 路徑,例如 /Products/Details/5

  • 您可以將路徑語彙基元化來擷取路由值 { controller = Products, action = Details, id = 5 }。 如果應用程式具有名為 ProductsController 的控制器和 Details 動作,擷取路由值就會導致相符:

    public class ProductsController : Controller
    {
        public IActionResult Details(int id)
        {
            return ControllerContext.MyDisplayRouteInfo(id);
        }
    }
    

    MyDisplayRouteInfo 是由 Rick.Docs.Samples.RouteInfo NuGet 套件提供,並顯示路由資訊。

  • /Products/Details/5 模型會繫結 id = 5 的值,將 id 參數設定為 5。 如需詳細資料,請參閱模型繫結

  • {controller=Home}Home 定義為預設的 controller

  • {action=Index}Index 定義為預設的 action

  • {id?} 中的 ? 字元會將 id 定義為選擇性。

  • 預設和選擇性路由參數不一定要全部出現在 URL 路徑中才算相符。 如需路由範本語法的詳細描述,請參閱路由範本參考

  • 符合 URL 路徑 /

  • 產生路由值 { controller = Home, action = Index }

controlleraction 的值會使用預設值。 id 不會產生值,因為 URL 路徑中沒有對應的區段。 / 僅在有 HomeControllerIndex 動作時才會相符:

public class HomeController : Controller
{
  public IActionResult Index() { ... }
}

使用上述控制器定義及路由範本,就會對下列 URL 路徑執行 HomeController.Index 動作:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /

URL 路徑 / 會使用路由範本預設 Home 控制器和 Index 動作。 URL 路徑 /Home 會使用路由範本預設 Index 動作。

MapDefaultControllerRoute 方法很方便:

endpoints.MapDefaultControllerRoute();

取代:

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

重要

路由是使用 UseRoutingMapControllerRouteMapAreaControllerRoute 中介軟體進行設定。 若要使用控制器:

慣例路由

慣例路由會與控制器和檢視搭配使用。 default 路由:

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

上述為「慣例路由」的範例。 其稱為「慣例路由」,因為其會建立 URL 路徑的「慣例」

  • 第一個路徑區段 {controller=Home} 對應至控制器名稱。
  • 第二個區段 {action=Index} 會對應至動作名稱。
  • 第三個區段 {id?} 用於選擇性的 id{id?} 中的 ? 會使其成為選擇性的。 id 是用來對應至模型實體。

使用此 default 路由,URL 路徑:

  • /Products/List 對應至 ProductsController.List 動作。
  • /Blog/Article/17 對應至 BlogController.Article,且通常模型會將 id 參數繫結至 17。

此對應:

  • 以控制器和動作名稱為基礎。
  • 不是以命名空間、來源檔案位置或方法參數為基礎。

使用慣例路由搭配預設路由可建立應用程式,而不必針對每個動作想出新的 URL 模式。 針對具有 CRUD 樣式動作的應用程式,在控制器之間具有 URL 的一致性:

  • 協助簡化程式碼。
  • 讓 UI 更容易預測。

警告

路由範本會將上述程式碼中的 id 定義為選擇性。 動作可以執行,而不需提供做為 URL 一部分的選擇性識別碼。 一般而言,當從 URL 省略時 id

  • id 是由模型繫結設定為 0
  • 在符合 id == 0 的資料庫中找不到任何實體。

屬性路由可提供更細微的控制,讓某些動作需要此識別碼,而其他動作則不需要。 依照慣例,本文件將會包含可能出現在正確使用中的選擇性參數 (例如 id)。

大部分應用程式都應該選擇基本的描述性路由傳送配置,讓 URL 可讀且有意義。 預設慣例路由 {controller=Home}/{action=Index}/{id?}

  • 支援基本的描述性路由配置。
  • 適合作為 UI 型應用程式的起點。
  • 這是許多 Web UI 應用程式唯一需要的路由範本。 對於較大的 Web UI 應用程式,經常只需要使用區域的另一個路由。

MapControllerRouteMapAreaRoute

  • 根據叫用端點的順序,自動將訂單值指派給其端點。

ASP.NET Core 3.0 和更新版本中的端點路由:

  • 沒有路由的概念。
  • 不會提供執行擴充性的順序保證,所有端點都會一次處理。

啟用記錄以查看內建路由實作 (例如 Route) 如何符合要求。

本文件稍後會說明屬性路由

多個慣例路由

您可以藉由對 和 MapAreaControllerRoute新增更多呼叫,在MapControllerRoute 內部 UseEndpoints 新增多個 慣例路由 。 這樣做可讓您定義多個慣例,或新增特定動作專用的慣例路由,例如:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});

上述程式碼中的 blog 路由是專用的慣例路由。 其稱為專用的慣例路由,因為:

因為 controlleraction 不會出現在路由範本 "blog/{*article}" 中做為參數:

  • 其只能有預設值 { controller = "Blog", action = "Article" }
  • 此路由一律會對應至動作 BlogController.Article

/Blog/Blog/Article/Blog/{any-string} 是唯一符合部落格路由的 URL 路徑。

上述範例:

  • blog 路由的比對優先順序高於 default 路由,因為其會先新增。
  • 這是 Slug 樣式路由的範例,其中通常會有文章名稱做為 URL 的一部分。

警告

在 ASP.NET Core 3.0 和更新版本中,路由不會:

  • 定義稱為路由的概念。 UseRouting 會將路由比對新增至中介軟體管線。 UseRouting 中介軟體會查看應用程式中定義的端點集合,並根據要求選取最佳端點比對。
  • 提供 IRouteConstraintIActionConstraint 等擴充性執行順序的保證。

如需路由的參考資料,請參閱路由

慣例路由順序

慣例路由只會比對應用程式所定義的動作和控制器組合。 這目的在於簡化慣例路由重疊的情況。 使用 MapControllerRouteMapDefaultControllerRouteMapAreaControllerRoute 新增路由,並根據叫用的順序,自動將訂單值指派給其端點。 來自稍早所出現路由的相符項目具有較高的優先順序。 慣例路由與順序息息相關。 一般而言,具有區域的路由應該放在前面,因為這些路由比沒有區域的路由更明確。 具有 catch-all 路由參數的專用慣例路由 (例如 {*article}) 可能會使路由變得太窮盡,這表示其符合您打算與其他路由比對的 URL。 將窮盡路由放在路由表中,以防止窮盡的相符項目。

警告

由於路由中的錯誤 (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.

解決語意模糊的動作

當透過路由比對兩個端點時,路由必須執行下列其中一項:

  • 選擇最佳候選項目。
  • 擲回例外狀況。

例如:

    public class Products33Controller : Controller
    {
        public IActionResult Edit(int id)
        {
            return ControllerContext.MyDisplayRouteInfo(id);
        }

        [HttpPost]
        public IActionResult Edit(int id, Product product)
        {
            return ControllerContext.MyDisplayRouteInfo(id, product.name);
        }
    }
}

上述控制器會定義兩個符合的動作:

  • URL 路徑 /Products33/Edit/17
  • 路由資料 { controller = Products33, action = Edit, id = 17 }

這是 MVC 控制器的典型模式:

  • Edit(int) 顯示要編輯產品的表單。
  • Edit(int, Product) 會處理張貼的表單。

若要解析正確的路由:

  • 當要求為 HTTP POST 時,會選取 Edit(int, Product)
  • Http 指令動詞是任何其他動作時,就會選取 Edit(int)Edit(int) 通常會透過 GET 進行呼叫。

會提供 HttpPostAttribute[HttpPost] 來路由,以便根據要求的 HTTP 方法進行選擇。 HttpPostAttribute 會使 Edit(int, Product) 成為比 Edit(int) 更佳的相符項目。

請務必了解屬性的角色,例如 HttpPostAttribute。 其他 Http 指令動詞會定義類似的屬性。 在慣例路由中,當動作是顯示表單、提交表單工作流程的一部分時,通常會使用相同的動作名稱。 例如,請參閱檢查兩個編輯動作方法

如果路由無法選擇最佳候選項目,則會擲回 AmbiguousMatchException,並列出多個相符的端點。

慣例路由名稱

下列範例中的字串 "blog""default" 是慣例路由名稱:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});

路由名稱會為路由提供邏輯名稱。 具名路由可用於產生 URL。 當路由排序可能使產生 URL 作業變得複雜時,使用具名路由可大幅簡化產生 URL 作業。 在整個應用程式內路由名稱必須是唯一的。

路由名稱:

  • 不會影響要求的 URL 比對或處理。
  • 僅用於產生 URL。

路由名稱概念是在路由中以 IEndpointNameMetadata 表示。 字詞路由名稱端點名稱

  • 可互換。
  • 文件與程式碼中會使用哪一個,取決於所描述的 API。

REST API 的屬性路由

REST API 應該使用屬性路由傳送來將應用程式功能模型建構為作業由 HTTP 指令動詞代表的資源集合。

屬性路由使用一組屬性,將動作直接對應至路由範本。 下列 StartUp.Configure 程式碼為 REST API 的一般用法,並用於下一個範例:

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

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

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

在上述程式碼中,會呼叫 UseEndpoints 內部 MapControllers 來對應屬性路由控制器。

在以下範例中:

  • HomeController 會比對一組類似預設慣例路由 {controller=Home}/{action=Index}/{id?} 相符的 URL。
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

HomeController.Index 動作會針對任何 URL 路徑 //Home/Home/Index/Home/Index/3 執行。

此範例醒目提示屬性路由與慣例路由之間的主要程式設計差異。 屬性路由需要更多輸入來指定路由。 慣例的預設路由會更簡潔地處理路由。 不過,屬性路由允許 (並需要) 精確地控制套用至每個動作的路由範本。

使用屬性路由時,除非使用語彙基元取代,否則控制器和動作名稱不會發揮比對動作的作用。 下列範例會比對與上一個範例相同的 URL:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

下列程式碼會使用 actioncontroller 的語彙基元取代 :

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

下列程式碼適用於 [Route("[controller]/[action]")] 控制器:

[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]
    [Route("/Home")]
    [Route("~/Home/Index")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

在上述程式碼中,Index 方法範本前面必須加上 /~/ 以路由範本。 套用至開頭為 /~/ 之動作的路由範本,無法與套用至控制器的路由範本合併。

如需路由範本選取項目的相關資訊,請參閱路由範本優先順序

保留的路由名稱

使用控制器或 Razor Pages 時,下列關鍵字是保留路由參數名稱:

  • action
  • area
  • controller
  • handler
  • page

使用 page 做為具有屬性路由的路由參數是常見的錯誤。 這麼做會導致產生 URL 的行為不一致且令人困惑。

public class MyDemo2Controller : Controller
{
    [Route("/articles/{page}")]
    public IActionResult ListArticles(int page)
    {
        return ControllerContext.MyDisplayRouteInfo(page);
    }
}

產生 URL 會使用特殊參數名稱來判斷 URL 產生作業是參考 Razor Page 還是 Controller。

下列關鍵字會保留於 Razor 檢視或 Razor Page 的內容中:

  • page
  • using
  • namespace
  • inject
  • section
  • inherits
  • model
  • addTagHelper
  • removeTagHelper

這些關鍵字不應該用於連結世代、模型繫結參數或最上層屬性。

Http 指令動詞範本

ASP.NET Core 具有下列 Http 指令動詞範本:

路由範本

ASP.NET Core 具有下列路由範本:

使用 Http 指令動詞屬性的屬性路由

請考慮下列控制器:

[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中:

  • 每個動作都包含 [HttpGet] 屬性,其只會限制與 HTTP GET 要求的比對。
  • GetProduct 動作包含 "{id}" 範本,因此 id 會附加至控制器上的 "api/[controller]" 範本。 方法範本為 "api/[controller]/{id}"。 因此,此動作只會比對表單 /api/test2/xyz/api/test2/123/api/test2/{any string} 等等的 GET 要求。
    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }
    
  • GetIntProduct 動作包含 "int/{id:int}" 範本。 範本的 :int 部分會將 id 路由值限制為可轉換成整數的字串。 對 /api/test2/int/abc 的 GET 要求:
    • 不符合此動作。
    • 傳回 404 找不到錯誤。
      [HttpGet("int/{id:int}")] // GET /api/test2/int/3
      public IActionResult GetIntProduct(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      
  • GetInt2Product 動作在範本中包含 {id},但不會將 id 限制為可轉換成整數的值。 對 /api/test2/int2/abc 的 GET 要求:
    • 符合此路由。
    • 模型繫結無法將 abc 轉換成整數。 方法的 id 參數是整數。
    • 傳回 400 不正確的要求,因為模型繫結無法將 abc 轉換成整數。
      [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
      public IActionResult GetInt2Product(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
      

屬性路由可以使用 HttpMethodAttribute 屬性,例如 HttpPostAttributeHttpPutAttributeHttpDeleteAttribute。 所有 Http 指令動詞屬性都接受路由範本。 下列範例顯示兩個符合相同路由範本的動作:

[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}

使用 URL 路徑 /products3

  • Http 指令動詞GET 時,MyProductsController.ListProducts 動作會執行。
  • Http 指令動詞POST 時,MyProductsController.CreateProduct 動作會執行。

建置 REST API 時,您很少需要在動作方法上使用 [Route(...)],因為動作會接受所有 HTTP 方法。 最好是使用更明確的 Http 指令動詞屬性,以精確地指定 API 的支援項目。 REST API 的用戶端必須知道哪些路徑和 HTTP 動詞命令對應至特定邏輯作業。

REST API 應該使用屬性路由傳送來將應用程式功能模型建構為作業由 HTTP 指令動詞代表的資源集合。 這表示相同邏輯資源上的許多作業 (例如,GET、POST) 都會使用相同的 URL。 屬性路由提供仔細設計 API 公用端點配置所需的控制層級。

由於屬性路由會套用至特定動作,因此輕鬆就能將參數設為路由範本定義的必要部分。 在下列範例中,需要 id 做為 URL 路徑的一部分:

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

Products2ApiController.GetProduct(int) 動作:

  • 使用 URL 路徑執行,例如 /products2/3
  • 不會使用 URL 路徑 /products2 執行。

[Consumes] 屬性可讓動作限制支援的要求內容類型。 如需詳細資訊,請參閱使用 Consumes 屬性定義支援的要求內容類型

如需路由範本和相關選項的完整描述,請參閱路由

如需 [ApiController] 的詳細資訊,請參閱 ApiController 屬性

路由名稱

下列程式碼會定義 Products_List 的路由名稱:

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

您可以使用路由名稱根據特定路由來產生 URL。 路由名稱:

  • 不會影響路由的 URL 比對行為。
  • 僅用於產生 URL。

在整個應用程式內路由名稱必須是唯一的。

上述程式碼與慣例預設路由相反,只會將 id 參數定義為選擇性 ({id?})。 能夠精確指定 API 有些優點,像是允許將 /products/products/5 分派至不同的動作。

合併屬性路由

為了避免屬性路由過於重複,控制器上的路由屬性可與個別動作上的路由屬性合併。 控制器上定義的任何路由範本都會附加到動作上的路由範本之前。 將路由屬性放在控制器上,即可讓控制器中的所有動作使用屬性路由。

[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在前述範例中:

  • URL 路徑 /products 可以比對 ProductsApi.ListProducts
  • URL 路徑 /products/5 可以比對 ProductsApi.GetProduct(int)

由於這兩種動作是以 [HttpGet] 屬性標示,因此只會符合 HTTP GET

套用至開頭為 /~/ 之動作的路由範本,無法與套用至控制器的路由範本合併。 下列範例會比對一組類似於預設路由的 URL 路徑。

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    [Route("/")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("About")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

下表說明上述程式碼中的 [Route] 屬性:

屬性 結合 [Route("Home")] 定義路由範本
[Route("")] Yes "Home"
[Route("Index")] .是 "Home/Index"
[Route("/")] ""
[Route("About")] Yes "Home/About"

屬性路由順序

路由會建置樹狀結構,並同時符合所有端點:

  • 路由項目的行為就像是放置在理想順序中一樣。
  • 最特定的路由有機會在較一般路由之前執行。

例如,類似 blog/search/{topic} 的屬性路由比 blog/{*article} 等屬性路由更具體。 blog/search/{topic} 路由預設具有較高的優先順序,因為其更具體。 使用慣例路由,開發人員會負責依想要的順序來排列路由。

屬性路由可以使用 Order 屬性來設定訂單。 所有架構提供的路由屬性都包含 Order。 路由會依 Order 屬性的遞增排序來處理。 預設順序為 0。 使用 Order = -1 設定的路由會在未設定順序的路由之前執行。 使用 Order = 1 設定的路由會在預設路由排序之後執行。

避免依賴 Order。 如果應用程式的 URL 空間需要明確的順序值才能正確地路由,則同樣也可能會使用戶端混淆。 一般而言,屬性路由會透過 URL 比對來選取正確的路由。 如果用於 URL 產生的預設順序無效,使用路由名稱做為覆寫通常會比套用 Order 屬性更簡單。

請考慮下列兩個控制器,這兩個控制器都會定義比對 /home 的路由:

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

使用上述程式碼要求 /home 會擲回類似下列的例外狀況:

AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex

Order 新增至其中一個路由屬性會解決語意模糊:

[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}

使用上述程式碼,/home 會執行 HomeController.Index 端點。 若要取得 MyDemoController.MyIndex,請要求 /home/MyIndex注意

  • 上述程式碼是範例或路由設計不佳。 其用來說明 Order 屬性。
  • 屬性 Order 只會解決語意模糊,並無法比對該範本。 最好移除 [Route("Home")] 範本。

如需使用 Razor Pages 的路由順序資訊,請參閱 Razor Pages 路由和應用程式慣例:路由順序

在某些情況下,HTTP 500 錯誤會以語意模糊的路由傳回。 使用記錄來查看導致 AmbiguousMatchException 的端點。

路由範本中的語彙基元取代 [controller]、[action]、[area]

為了方便起見,屬性路由支援以方括號 ([]) 括住語彙基元的「語彙基元取代」。 語彙基元 [action][area][controller] 會分別以定義路由之動作的動作名稱值、區域名稱值和控制器名稱值來取代:

[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中:

[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
  • 符合 /Products0/List
[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
  • 符合 /Products0/Edit/{id}

語彙基元取代會在建立屬性路由的最後一個步驟發生。 上述範例的行為與下列程式碼相同:

public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

如果您要以英文以外的語言閱讀本文,則若您想要以母語查看程式碼註解,請在此 GitHub 討論問題中告訴我們。

屬性路由也可以透過繼承來合併。 這與語彙基元取代結合會很強大。 語彙基元取代也適用於屬性路由所定義的路由名稱。 [Route("[controller]/[action]", Name="[controller]_[action]")] 會針對每個動作產生唯一的路由名稱:

[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

若要比對常值語彙基元取代分隔符號 [],請重複字元 ([[]]) 來將它逸出。

使用參數轉換程式自訂語彙基元取代

可以使用參數轉換程式自訂語彙基元取代。 參數轉換程式會實作 IOutboundParameterTransformer 並轉換參數值。 例如,自訂 SlugifyParameterTransformer 參數轉換器會將 SubscriptionManagement 路由值變更為 subscription-management

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();
    }
}

RouteTokenTransformerConvention 是一個應用程式模型慣例,它會:

  • 將參數轉換程式套用到應用程式中的所有屬性路由。
  • 在取代屬性路由語彙基元值時自訂它們。
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")]
    public IActionResult ListAll()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

上述 ListAll 方法符合 /subscription-management/list-all

RouteTokenTransformerConvention 會在 ConfigureServices 中註冊為選項。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.Conventions.Add(new RouteTokenTransformerConvention(
                                     new SlugifyParameterTransformer()));
    });
}

如需 Slug 的定義,請參閱 Slug 上的 MDN Web 文件

警告

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

多個屬性路由

屬性路由支援定義多個路由來達到相同的動作。 最常見的用法是模擬「預設慣例路由」的行為,如下列範例所示:

[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

將多個路由屬性放在控制器上,表示這些屬性會各自與動作方法上的每個路由屬性合併:

[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

所有 Http 指令動詞路由條件約束都會實作 IActionConstraint

當實作的 IActionConstraint 多個路由屬性放在動作上時:

  • 每個動作條件約束都會結合套用至控制器的路由範本。
[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

在動作上使用多個路由看似很有用且強大,但最好保持基本且妥善定義的應用程式 URL 空間。 只有在必要時,才在動作上使用多個路由;例如,為了支援現有的用戶端。

指定屬性路由的選擇性參數、預設值和條件約束

屬性路由支援使用與慣例路由相同的內嵌語法,來指定選擇性參數、預設值和條件約束。

public class Products14Controller : Controller
{
    [HttpPost("product14/{id:int}")]
    public IActionResult ShowProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在上述程式碼中,[HttpPost("product14/{id:int}")] 會套用路由條件約束。 Products14Controller.ShowProduct 動作只會由 URL 路徑比對,例如 /product14/3。 路由範本部分 {id:int} 會將該區段限制為只有整數。

如需路由範本語法的詳細描述,請參閱路由範本參考

使用 IRouteTemplateProvider 的自訂路由屬性

所有路由屬性都會實作 IRouteTemplateProvider。 ASP.NET Core 執行階段:

  • 在應用程式啟動時,尋找控制器類別和動作方法的屬性。
  • 使用實作 IRouteTemplateProvider 的屬性來建置初始路由集。

實作 IRouteTemplateProvider 以定義自訂路由屬性。 每個 IRouteTemplateProvider 都可讓您定義具有自訂路由範本、順序和名稱的單一路由:

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";
    public int? Order => 2;
    public string Name { get; set; }
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
    // GET /api/MyTestApi
    [HttpGet]
    public IActionResult Get()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

上述 Get 方法會傳回 Order = 2, Template = api/MyTestApi

使用應用程式模型自訂屬性路由

應用程式模型:

  • 這是啟動時所建立的物件模型。
  • 包含 ASP.NET Core 用來路由及執行應用程式中動作的所有中繼資料。

應用程式模型包含從路由屬性收集的所有資料。 IRouteTemplateProvider 實作會提供來自路由屬性的資料。 慣例:

  • 您可以撰寫來修改應用程式模型,以自訂路由的行為。
  • 在應用程式啟動時讀取。

本節說明使用應用程式模型來自訂路由的基本範例。 下列程式碼會讓路由大致與專案的資料夾結構對齊。

public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector =>
                                                selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            return;
        }

        var namespc = controller.ControllerType.Namespace;
        if (namespc == null)
            return;
        var template = new StringBuilder();
        template.Append(namespc, _baseNamespace.Length + 1,
                        namespc.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]/[action]/{id?}");

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString()
            };
        }
    }
}

下列程式碼可防止 namespace 慣例套用至路由屬性的控制器:

public void Apply(ControllerModel controller)
{
    var hasRouteAttributes = controller.Selectors.Any(selector =>
                                            selector.AttributeRouteModel != null);
    if (hasRouteAttributes)
    {
        return;
    }

例如,下列控制器不使用 NamespaceRoutingConvention

[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
    // /managers/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        return Content($"Index- template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

NamespaceRoutingConvention.Apply 方法:

  • 如果控制器路由屬性,則不會執行任何動作。
  • 根據 namespace 設定控制器範本,並移除基底 namespace

您可以在 Startup.ConfigureServices 中套用 NamespaceRoutingConvention

namespace My.Application
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews(options =>
            {
                options.Conventions.Add(
                    new NamespaceRoutingConvention(typeof(Startup).Namespace));
            });
        }
        // Remaining code ommitted for brevity.

例如,請考量下列控制器:

using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
    public class UsersController : Controller
    {
        // GET /admin/controllers/users/index
        public IActionResult Index()
        {
            var fullname = typeof(UsersController).FullName;
            var template = 
                ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
            var path = Request.Path.Value;

            return Content($"Path: {path} fullname: {fullname}  template:{template}");
        }

        public IActionResult List(int? id)
        {
            var path = Request.Path.Value;
            return Content($"Path: {path} ID:{id}");
        }
    }
}

在上述程式碼中:

  • 基底 namespaceMy.Application
  • 上述控制器的完整名稱為 My.Application.Admin.Controllers.UsersController
  • NamespaceRoutingConvention 會將控制器範本設定為 Admin/Controllers/Users/[action]/{id?

NamespaceRoutingConvention 也可以套用為控制器上的屬性:

[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
    // /admin/controllers/test/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        var actionname = ControllerContext.ActionDescriptor.ActionName;
        return Content($"Action- {actionname} template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}

混合路由:屬性路由與慣例路由

ASP.NET Core 應用程式可以混用慣例路由與屬性路由。 控制器通常會使用慣例路由來提供 HTML 頁面給瀏覽器,並使用屬性路由來提供 REST API。

動作可以使用慣例路由或屬性路由。 將路由放在控制器或動作上,即可讓它使用屬性路由。 定義屬性路由的動作無法透過慣例路由到達,反之亦然。 控制器上的任何路由屬性可讓控制器中的所有動作使用屬性路由。

屬性路由和慣例路由使用相同的路由引擎。

產生 URL 和環境值

應用程式可以使用路由的 URL 產生功能,來產生動作的 URL 連結。 產生 URL 可排除硬式編碼的 URL,讓程式碼更穩定且更容易維護。 本節著重於 MVC 所提供的 URL 產生功能,並只會涵蓋 URL 產生運作方式的基本概念。 如需 URL 產生的詳細描述,請參閱路由

IUrlHelper 介面是 MVC 與用於產生 URL 的路由之間的基礎結構元素。 透過控制器、檢視和檢視元件中 Url 屬性,來尋找可用的 IUrlHelper 執行個體。

在下列範例中,會透過 Controller.Url 屬性使用 IUrlHelper 介面來產生另一個動作的 URL。

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

如果應用程式使用預設慣例路由,url 變數的值會是 URL 路徑字串 /UrlGeneration/Destination。 此 URL 路徑是結合下列方式加以建立:

  • 目前要求的路由值,稱為環境值
  • 將值傳遞給 Url.Action 並將這些值取代為路由範本:
ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

路由範本中每個路由參數的值都會以相符名稱的值和環境值所取代。 沒有值的路由參數可以:

  • 如果有預設值,請使用預設值。
  • 如果這是選擇性的,請略過。 例如,來自路由範本 {controller}/{action}/{id?}id

如果任何必要的路由參數沒有對應的值,URL 產生會失敗。 如果某個路由的 URL 產生失敗,則會嘗試下一個路由,直到嘗試所有路由或找到相符項目為止。

上述 Url.Action 範例假設使用慣例路由。 雖然概念不同,但產生 URL 的運作方式與屬性路由類似。 使用慣例路由:

  • 路由值可用來展開範本。
  • controlleraction 的路由值通常會出現在該範本中。 這行得通,因為路由所比對的 URL 會遵守慣例。

下列範例使用屬性路由:

public class UrlGenerationAttrController : Controller
{
    [HttpGet("custom")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination()
    {
       return ControllerContext.MyDisplayRouteInfo();
    }
}

上述程式碼中的 Source 動作會產生 custom/url/to/destination

已在 ASP.NET Core 3.0 中新增 LinkGenerator,做為 IUrlHelper 的替代方案。 LinkGenerator 提供類似但更具彈性的功能。 IUrlHelper 上的每個方法在 LinkGenerator 上也有對應的方法系列。

由動作名稱產生 URL

Url.ActionLinkGenerator.GetPathByAction 和所有相關多載全都設計成透過指定控制器名稱和動作名稱來產生目標端點。

使用 Url.Action 時,執行階段會提供 controlleraction 的目前路由值:

  • controlleraction 的值同時屬於環境值和值。 方法 Url.Action 一律會使用 actioncontroller 的目前值,且會產生路由至目前動作的 URL 路徑。

路由會嘗試使用環境值中的值,來填入在產生 URL 時所未提供的資訊。 請考慮使用 {a}/{b}/{c}/{d} 之類的路由搭配 { a = Alice, b = Bob, c = Carol, d = David } 環境值:

  • 路由有足夠的資訊來產生 URL,而不需要任何額外的值。
  • 路由有足夠的資訊,因為所有路由參數都有一個值。

如果已新增值 { d = Donovan }

  • 會忽略 { d = David } 值。
  • 產生的 URL 路徑為 Alice/Bob/Carol/Donovan

警告:URL 路徑是階層式。 在上述範例中,如果已新增值 { c = Cheryl }

  • 會忽略 { c = Carol, d = David } 這兩個值。
  • 不再有 d 的值,且產生 URL 失敗。
  • 必須指定 cd 的所需值,才能產生 URL。

您可能預期會使用預設路由 {controller}/{action}/{id?} 來解決此問題。 此問題在實務上很少見,因為 Url.Action 一律會明確指定 controlleraction 值。

數個 Url.Action 多載會接受一個路由值物件,來為 controlleraction 以外的路由參數提供值。 路由值物件經常與 id 搭配使用。 例如: Url.Action("Buy", "Products", new { id = 17 }) 。 路由值物件:

  • 依照慣例,通常是匿名型別的物件。
  • 可以是 IDictionary<>POCO)。

不符合路由參數的任何額外路由值都會放在查詢字串中。

public IActionResult Index()
{
    var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
    return Content(url);
}

上述程式碼會產生 /Products/Buy/17?color=red

下列程式碼會產生絕對 URL:

public IActionResult Index2()
{
    var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
    // Returns https://localhost:5001/Products/Buy/17
    return Content(url);
}

若要建立絕對 URL,請使用下列其中一項:

依路由產生 URL

上述程式碼示範如何藉由傳入控制器和動作名稱來產生 URL。 IUrlHelper 也提供方法的 Url.RouteUrl 系列。 這些方法類似於 Url.Action,但不會將 actioncontroller 的目前值複製到路由值。 最常見的 Url.RouteUrl 用法:

  • 指定要產生 URL 的路由名稱。
  • 通常不會指定控制器或動作名稱。
public class UrlGeneration2Controller : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

下列 Razor 檔案會產生 Destination_Route 的 HTML 連結:

<h1>Test Links</h1>

<ul>
    <li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>

在 HTML 和 Razor 中產生 URL

IHtmlHelper 提供 Html.BeginFormHtml.ActionLinkHtmlHelper 方法,分別產生 <form><a> 元素。 這些方法使用 Url.Action 方法來產生 URL,並接受類似的引數。 HtmlHelper 的成對 Url.RouteUrlHtml.BeginRouteFormHtml.RouteLink,這兩者的功能很類似。

TagHelper 透過 form TagHelper 和 <a> TagHelper 產生 URL。 這兩者使用 IUrlHelper 進行實作。 如需詳細資訊,請參閱表單中的標籤協助程式

在檢視中,可透過 Url 屬性使用 IUrlHelper 來產生上述未涵蓋的任何特定 URL。

在動作結果中產生 URL

以上顯示的範例在控制器中使用 IUrlHelper。 控制器中最常見的用法是產生 URL 做為動作結果的一部分。

ControllerBaseController 基底類別提供便利的方法讓動作結果可參考其他動作。 一個典型的用法是在接受使用者輸入之後重新導向:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        ViewData["Message"] = $"Successful edit of customer {id}";
        return RedirectToAction("Index");
    }
    return View(customer);
}

動作結果的 Factory 方法 (例如 RedirectToActionCreatedAtAction) 遵循類似於 IUrlHelper 上方法的模式。

專用慣例路由的特殊案例

慣例路由可使用一種特殊路由定義,稱為專用慣例路由。 在下列範例中,名為 blog 的路由是專用慣例路由:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});

使用上述路由定義,Url.Action("Index", "Home") 會使用 default 路由來產生 URL 路徑 /,但原因為何? 您可能會猜想路由值 { controller = Home, action = Index } 便足以使用 blog 來產生 URL,且結果會是 /blog?action=Index&controller=Home

專用慣例路由依賴沒有對應路由參數預設值的特殊行為,以防止用於 URL 產生的路由變得太窮盡。 在本例中,預設值為 { controller = Blog, action = Article }controlleraction 都不會顯示為路由參數。 當路由執行 URL 產生時,所提供的值必須符合預設值。 使用 blog 產生 URL 會失敗,因為值 { controller = Home, action = Index } 不符合 { controller = Blog, action = Article }。 路由會接著切換並嘗試 default,此時會成功。

領域

區域是一項 MVC 功能,用來將相關功能組織成群組以進行區隔:

  • 控制器動作的路由命名空間。
  • 檢視的資料夾結構。

使用區域可讓應用程式具有多個同名的控制器,只要這些控制器具有不同的區域即可。 使用區域可建立用於路由的階層,方法是將另一個路由參數 area 新增至 controlleraction。 本節討論路由如何與區域互動。 如需如何搭配檢視使用區域的詳細資訊,請參閱區域

下列範例會設定 MVC 使用預設慣例路由,並為名為 Blogarea 設定 area 路由:

app.UseEndpoints(endpoints =>
{
    endpoints.MapAreaControllerRoute("blog_route", "Blog",
        "Manage/{controller}/{action}/{id?}");
    endpoints.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
});

在上述程式碼中,會呼叫 MapAreaControllerRoute 以建立 "blog_route"。 第二個參數 "Blog" 是區域名稱。

當符合 /Manage/Users/AddUser 等 URL 路徑時,"blog_route" 路由會產生路由值 { area = Blog, controller = Users, action = AddUser }area 路由值是由 area 的預設值所產生。 MapAreaControllerRoute 所建立的路由相當於下列項目:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
    endpoints.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
});

MapAreaControllerRoute 會針對使用所提供之區域名稱 (在本例中為 Blog) 的 area,使用預設值和條件約束來建立路由。 預設值可確保路由一律會產生 { area = Blog, ... },而條件約束需要 { area = Blog, ... } 值以產生 URL。

慣例路由與順序息息相關。 一般而言,具有區域的路由應該放在前面,因為這些路由比沒有區域的路由更明確。

使用上述範例,路由值 { area = Blog, controller = Users, action = AddUser } 符合下列動作:

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}

[Area] 屬性代表控制器做為區域的一部分。 此控制器位於 Blog 區域中。 不含 [Area] 屬性的控制器不是任何區域的成員,因此當路由提供 area 路由值時不會符合。 在下列範例中,只有列出的第一個控制器可能符合路由值 { area = Blog, controller = Users, action = AddUser }

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        // GET /zebra/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        // GET /users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }
    }
}

這裡會顯示每個控制器的命名空間,以取得完整性。 如果上述控制器使用相同的命名空間,則會產生編譯器錯誤。 類別命名空間不會影響 MVC 的路由。

前兩個控制器是區域的成員,只有在 area 路由值提供其各自的區域名稱時才會符合。 第三個控制器不是任何區域的成員,只有路由未提供任何值給 area 時才會符合。

在「未符合任何值」的情況下,缺少 area 值相當於 area 的值為 Null 或空字串。

執行區域中的動作時,area 的路由值會作為路由用於 URL 產生的「環境值」。 這表示區域預設會以「黏性」方式來處理 URL 產生,如下列範例所示。

app.UseEndpoints(endpoints =>
{
    endpoints.MapAreaControllerRoute(name: "duck_route", 
                                     areaName: "Duck",
                                     pattern: "Manage/{controller}/{action}/{id?}");
    endpoints.MapControllerRoute(name: "default",
                                 pattern: "Manage/{controller=Home}/{action=Index}/{id?}");
});
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
    [Area("Duck")]
    public class UsersController : Controller
    {
        // GET /Manage/users/GenerateURLInArea
        public IActionResult GenerateURLInArea()
        {
            // Uses the 'ambient' value of area.
            var url = Url.Action("Index", "Home");
            // Returns /Manage/Home/Index
            return Content(url);
        }

        // GET /Manage/users/GenerateURLOutsideOfArea
        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area.
            var url = Url.Action("Index", "Home", new { area = "" });
            // Returns /Manage
            return Content(url);
        }
    }
}

下列程式碼會產生 /Zebra/Users/AddUser 的 URL:

public class HomeController : Controller
{
    public IActionResult About()
    {
        var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
        return Content($"URL: {url}");
    }

動作定義

控制器上的公用方法 (不包括以 NonAction 屬性裝飾的項目) 就是動作。

範例指令碼

偵錯診斷

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

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