在 ASP.NET Core 中路由到控制器操作Routing to controller actions in ASP.NET Core

本文内容

作者: Ryan NowakKirk LarkinRick Anderson

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 url,并将其映射到操作路由模板:

  • 在启动代码或属性中定义。
  • 描述 URL 路径如何与操作匹配。
  • 用于生成链接的 Url。生成的链接通常在响应中返回。

操作是按逆路由或按属性路由在控制器或操作上放置路由,使其属性路由。有关详细信息,请参阅混合路由

本文档:

  • 说明 MVC 与路由之间的交互:
    • 典型的 MVC 应用使用路由功能的方式。
    • 涵盖两种:
    • 有关高级路由的详细信息,请参阅路由
  • 指 ASP.NET Core 3.0 中添加的默认路由系统,称为 "终结点路由"。出于兼容性目的,可以将控制器用于以前版本的路由。有关说明,请参阅2.2-3.0 迁移指南。有关旧路由系统上的参考材料,请参阅本文档的2.2 版本

设置传统路由Set up conventional route

使用传统路由时,Startup.Configure 通常具有如下所示的代码:

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

在对 UseEndpoints的调用中,MapControllerRoute 用于创建单个路由。单路由命名 default 路由。大多数具有控制器和视图的应用都使用类似于 default 路由的路由模板。REST Api 应使用属性路由

路由模板 "{controller=Home}/{action=Index}/{id?}"

  • 匹配 URL 路径 /Products/Details/5
  • 通过词汇切分路径提取 { controller = Products, action = Details, id = 5 } 的路由值。如果应用具有名为 ProductsControllerDetails 操作的控制器,则提取路由值将导致匹配:
  1. public class ProductsController : Controller
  2. {
  3. public IActionResult Details(int id)
  4. {
  5. return ControllerContext.MyDisplayRouteInfo(id);
  6. }
  7. }

示例下载中包含了MyDisplayRouteInfo方法,用于显示路由信息。

  • /Products/Details/5 模型将 id = 5 的值绑定,以将 id 参数设置为 5。有关更多详细信息,请参阅模型绑定
  • {controller=Home}Home 定义为默认的 controller
  • {action=Index}Index 定义为默认的 action
  • {id?} 中的 ? 字符将 id 定义为可选。
  • 默认路由参数和可选路由参数不必包含在 URL 路径中进行匹配。有关路由模板语法的详细说明,请参阅路由模板参考
  • 匹配 /的 URL 路径。
  • { controller = Home, action = Index }生成路由值。
  • 示例下载中包含了MyDisplayRouteInfo方法,用于显示路由信息。

controlleraction 的值将使用默认值。id 不会生成值,因为 URL 路径中没有相应的段。仅当存在 HomeControllerIndex 操作时,/ 才匹配:

  1. public class HomeController : Controller
  2. {
  3. public IActionResult Index() { ... }
  4. }

使用上述控制器定义和路由模板,为以下 URL 路径运行 HomeController.Index 操作:

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

URL 路径 / 使用路由模板默认 Home 控制器和 Index 操作。URL 路径 /Home 使用路由模板默认 Index 操作。

简便方法 MapDefaultControllerRoute

  1. endpoints.MapDefaultControllerRoute();

代替

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

使用 UseRoutingUseEndpoints 中间件配置路由。使用控制器:

传统路由Conventional routing

传统路由用于控制器和视图。default 路由:

  1. endpoints.MapControllerRoute(
  2. name: "default",
  3. 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 的一部分提供的可选 ID 即可执行操作。通常,从 URL 中省略id 时:

  • id 设置为通过模型绑定 0
  • 在数据库匹配 id == 0中找不到实体。

特性路由可提供精细的控制,以使某些操作(而不是其他操作)需要 ID。按照约定,文档中包含的可选参数(如 id)可能会出现正确用法。

大多数应用应选择基本的描述性路由方案,让 URL 有可读性和意义。默认传统路由 {controller=Home}/{action=Index}/{id?}

  • 支持基本的描述性路由方案。
  • 是基于 UI 的应用的有用起点。
  • 是许多 web UI 应用程序所需的唯一路由模板。对于较大的 web UI 应用,如果经常需要,则使用区域的其他路由。

MapControllerRouteMapAreaRoute

  • 根据调用的顺序,自动将订单值分配给其终结点。

Endpoint 路由 ASP.NET Core 3.0 及更高版本:

  • 没有路由的概念。
  • 不为执行扩展性提供排序保证,同时处理所有终结点。

启用日志记录以查看内置路由实现(如 Route)如何匹配请求。

属性路由将在本文档的后面部分进行说明。

多个传统路由Multiple conventional routes

通过添加对 MapControllerRouteMapAreaControllerRoute的更多调用,可以将多个传统路由添加到 UseEndpoints 中。这样做允许定义多个约定,或者添加专用于特定操作的传统路由,例如:

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

上述代码中的 blog 路由是专用的传统路由这称为专用的传统路由,因为:

因为 controlleraction 不会以参数形式出现在路由 "blog/{*article}" 模板中:

  • 它们只能 { controller = "Blog", action = "Article" }默认值。
  • 此路由始终映射到操作 BlogController.Article

/Blog/Blog/Article/Blog/{any-string} 只是与博客路由匹配的 URL 路径。

前面的示例:

  • blog 路由的优先级高于 default 路由,因为它是首先添加的。
  • 是一个信息区样式路由的示例,其中通常将项目名称作为 URL 的一部分。

警告

在 ASP.NET Core 3.0 及更高版本中,路由不会:

  • 定义名为route的概念。UseRouting 将路由匹配添加到中间件管道。UseRouting 中间件会查看应用中定义的终结点集,并根据请求选择最佳的终结点匹配。
  • 提供可扩展性(如 IRouteConstraintIActionConstraint)的执行顺序。

有关路由的参考材料,请参阅路由

传统路由顺序Conventional routing order

传统路由仅匹配应用定义的操作和控制器的组合。这旨在简化传统路由重叠的情况。使用 MapControllerRouteMapDefaultControllerRouteMapAreaControllerRoute 添加路由时,会根据调用顺序,自动将订单值分配给其终结点。与前面显示的路由的匹配具有更高的优先级。传统路由依赖于顺序。通常情况下,应将具有区域的路由置于更早的位置,因为它们比没有区域的路由更具体。使用捕获所有路由参数(如 {*article})的专用传统路由可以使路由过于贪婪,这意味着它会匹配你打算被其他路由匹配的 url。将贪婪路由置于路由表中,以防止贪婪匹配。

解决不明确的操作Resolving ambiguous actions

如果两个终结点通过路由匹配,则路由必须执行下列操作之一:

  • 选择最佳的候选项。
  • 引发异常。

例如:

  1. public class Products33Controller : Controller
  2. {
  3. public IActionResult Edit(int id)
  4. {
  5. return ControllerContext.MyDisplayRouteInfo(id);
  6. }
  7. [HttpPost]
  8. public IActionResult Edit(int id, Product product)
  9. {
  10. return ControllerContext.MyDisplayRouteInfo(id, product.name);
  11. }
  12. }
  13. }

前面的控制器定义了两个匹配的操作:

  • URL 路径 /Products33/Edit/17
  • { controller = Products33, action = Edit, id = 17 }路由数据。

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

  • Edit(int) 显示用于编辑产品的窗体。
  • Edit(int, Product) 处理已发布的窗体。

解析正确的路由:

  • 当请求为 HTTP POST时选择 Edit(int, Product)
  • 如果HTTP 谓词为其他任何内容,则选择 Edit(int)。通常通过 GET调用 Edit(int)

提供了 HttpPostAttribute[HttpPost]以便路由,以便可以根据请求的 HTTP 方法进行选择。HttpPostAttribute 使 Edit(int, Product)Edit(int)更好的匹配项。

了解属性(如 HttpPostAttribute)的角色非常重要。为其他HTTP 谓词定义了类似的属性。传统路由中,当操作是显示形式的一部分时,操作通常使用相同的操作名称,即提交窗体工作流。例如,请参阅检查两个编辑操作方法

如果路由无法选择最佳候选项,则会引发 AmbiguousMatchException,并列出多个匹配的终结点。

传统路由名称Conventional route names

以下示例中的字符串 "blog""default" 是传统的路由名称:

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

路由名称为路由指定逻辑名称。命名路由可用于生成 URL。当路由顺序使 URL 生成复杂化时,使用命名路由可简化 URL 创建。路由名称必须是唯一的应用程序范围。

路由名称:

  • 不会影响 URL 匹配或处理请求。
  • 仅用于生成 URL。

路由名称概念在路由中表示为IEndpointNameMetadata术语路由名称终结点名称

  • 是可互换的。
  • 文档和代码中使用哪一个取决于所述的 API。

REST Api 的属性路由Attribute routing for REST APIs

REST Api 应使用属性路由将应用功能建模为一组资源,其中的操作由HTTP 谓词表示。

属性路由使用一组属性将操作直接映射到路由模板。下面的 StartUp.Configure 代码是 REST API 的典型代码,并在下一个示例中使用:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddControllers();
  4. }
  5. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  6. {
  7. if (env.IsDevelopment())
  8. {
  9. app.UseDeveloperExceptionPage();
  10. }
  11. app.UseHttpsRedirection();
  12. app.UseRouting();
  13. app.UseAuthorization();
  14. app.UseEndpoints(endpoints =>
  15. {
  16. endpoints.MapControllers();
  17. });
  18. }

在前面的代码中,在 UseEndpoints 内调用 MapControllers,以映射属性路由控制器。

如下示例中:

  • 使用前面的 Configure 方法。
  • HomeController 匹配一组 Url,类似于默认的传统路由 {controller=Home}/{action=Index}/{id?} 匹配的内容。
  1. public class HomeController : Controller
  2. {
  3. [Route("")]
  4. [Route("Home")]
  5. [Route("Home/Index")]
  6. [Route("Home/Index/{id?}")]
  7. public IActionResult Index(int? id)
  8. {
  9. return ControllerContext.MyDisplayRouteInfo(id);
  10. }
  11. [Route("Home/About")]
  12. [Route("Home/About/{id?}")]
  13. public IActionResult About(int? id)
  14. {
  15. return ControllerContext.MyDisplayRouteInfo(id);
  16. }
  17. }

//Home/Home/Index/Home/Index/3的任何 URL 路径运行 HomeController.Index 操作。

此示例突出显示了属性路由与传统路由之间的主要编程区别。属性路由需要更多输入才能指定路由。传统的默认路由会更简洁地处理路由。但是,特性路由允许和要求精确控制哪些路由模板适用于每个操作

对于属性路由,控制器名称和操作名称扮演操作匹配的角色。下面的示例匹配与上一示例相同的 Url:

  1. public class MyDemoController : Controller
  2. {
  3. [Route("")]
  4. [Route("Home")]
  5. [Route("Home/Index")]
  6. [Route("Home/Index/{id?}")]
  7. public IActionResult MyIndex(int? id)
  8. {
  9. return ControllerContext.MyDisplayRouteInfo(id);
  10. }
  11. [Route("Home/About")]
  12. [Route("Home/About/{id?}")]
  13. public IActionResult MyAbout(int? id)
  14. {
  15. return ControllerContext.MyDisplayRouteInfo(id);
  16. }
  17. }

下面的代码对 actioncontroller使用标记替换:

  1. public class HomeController : Controller
  2. {
  3. [Route("")]
  4. [Route("Home")]
  5. [Route("[controller]/[action]")]
  6. public IActionResult Index()
  7. {
  8. return ControllerContext.MyDisplayRouteInfo();
  9. }
  10. [Route("[controller]/[action]")]
  11. public IActionResult About()
  12. {
  13. return ControllerContext.MyDisplayRouteInfo();
  14. }
  15. }

下面的代码将 [Route("[controller]/[action]")] 应用到控制器:

  1. public class HomeController : Controller
  2. {
  3. public IActionResult Index()
  4. {
  5. return ControllerContext.MyDisplayRouteInfo();
  6. }
  7. }

在前面的代码中,Index 方法模板必须在路由模板之前 /~/应用于操作的以 /~/ 开头的路由模板不与应用于控制器的路由模板合并。

有关路由模板选择的信息,请参阅路由模板优先级

保留的路由名称Reserved routing names

使用控制器或 Razor Pages 时,以下关键字为保留路由参数名称:

  • action
  • area
  • controller
  • handler
  • page

page 用作具有属性路由的路由参数是一个常见错误。这样做会导致在 URL 生成时出现不一致的行为。

  1. public class MyDemo2Controller : Controller
  2. {
  3. [Route("/articles/{page}")]
  4. public IActionResult ListArticles(int page)
  5. {
  6. return ControllerContext.MyDisplayRouteInfo(page);
  7. }
  8. }

URL 生成使用特殊参数名称来确定 URL 生成操作是指 Razor 页面还是引用控制器。

HTTP 谓词模板HTTP verb templates

ASP.NET Core 具有以下 HTTP 谓词模板:

路由模板Route templates

ASP.NET Core 具有以下路由模板:

具有 Http 谓词特性的属性路由Attribute routing with Http verb attributes

请考虑以下控制器:

  1. [Route("api/[controller]")]
  2. [ApiController]
  3. public class Test2Controller : ControllerBase
  4. {
  5. [HttpGet] // GET /api/test2
  6. public IActionResult ListProducts()
  7. {
  8. return ControllerContext.MyDisplayRouteInfo();
  9. }
  10. [HttpGet("{id}")] // GET /api/test2/xyz
  11. public IActionResult GetProduct(string id)
  12. {
  13. return ControllerContext.MyDisplayRouteInfo(id);
  14. }
  15. [HttpGet("int/{id:int}")] // GET /api/test2/int/3
  16. public IActionResult GetIntProduct(int id)
  17. {
  18. return ControllerContext.MyDisplayRouteInfo(id);
  19. }
  20. [HttpGet("int2/{id}")] // GET /api/test2/int2/3
  21. public IActionResult GetInt2Product(int id)
  22. {
  23. return ControllerContext.MyDisplayRouteInfo(id);
  24. }
  25. }

在上述代码中:

  • 每个操作都包含 [HttpGet] 特性,该特性仅将匹配限制为 HTTP GET 请求。
  • GetProduct 操作包括 "{id}" 模板,因此 id 追加到控制器上的 "api/[controller]" 模板。"api/[controller]/"{id}""方法模板。因此,此操作仅将 /api/test2/xyz/api/test2/123/api/test2/{any string}等窗体的 GET 请求匹配。
  1. [HttpGet("{id}")] // GET /api/test2/xyz
  2. public IActionResult GetProduct(string id)
  3. {
  4. return ControllerContext.MyDisplayRouteInfo(id);
  5. }
  • GetIntProduct 操作包含 "int/{id:int}") 模板。模板的 :int 部分将 id 路由值限制为可以转换为整数的字符串。要 /api/test2/int/abc的 GET 请求:
  1. [HttpGet("int/{id:int}")] // GET /api/test2/int/3
  2. public IActionResult GetIntProduct(int id)
  3. {
  4. return ControllerContext.MyDisplayRouteInfo(id);
  5. }
  • GetInt2Product 操作包含模板中的 {id},但不会将 id 限制为可转换为整数的值。要 /api/test2/int2/abc的 GET 请求:
    • 与此路由匹配。
    • 模型绑定无法将 abc 转换为整数。此方法的 id 参数是整数。
    • 返回400 错误请求,因为模型绑定未能将abc 转换为整数。
  1. [HttpGet("int2/{id}")] // GET /api/test2/int2/3
  2. public IActionResult GetInt2Product(int id)
  3. {
  4. return ControllerContext.MyDisplayRouteInfo(id);
  5. }

特性路由可以使用 HttpMethodAttribute 特性,如 HttpPostAttributeHttpPutAttributeHttpDeleteAttribute所有HTTP 谓词特性都接受路由模板。下面的示例演示两个匹配同一路由模板的操作:

  1. [ApiController]
  2. public class MyProductsController : ControllerBase
  3. {
  4. [HttpGet("/products3")]
  5. public IActionResult ListProducts()
  6. {
  7. return ControllerContext.MyDisplayRouteInfo();
  8. }
  9. [HttpPost("/products3")]
  10. public IActionResult CreateProduct(MyProduct myProduct)
  11. {
  12. return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
  13. }
  14. }

使用 URL 路径 /products3

  • GETHTTP 谓词时,MyProductsController.ListProducts 操作将运行。
  • POSTHTTP 谓词时,MyProductsController.CreateProduct 操作将运行。

构建 REST API 时,很少需要在操作方法上使用 [Route(…)],因为操作接受所有 HTTP 方法。更好的方法是使用更具体的HTTP 谓词特性来精确了解 API 支持的内容。REST API 的客户端需要知道映射到特定逻辑操作的路径和 Http 谓词。

REST Api 应使用属性路由将应用功能建模为一组资源,其中的操作由 HTTP 谓词表示。这意味着,多个操作(例如,同一逻辑资源的 GET 和 POST)使用相同的 URL。属性路由提供了精心设计 API 的公共终结点布局所需的控制级别。

由于属性路由适用于特定操作,因此,使参数变成路由模板定义中的必需参数很简单。在下面的示例中,需要 id 作为 URL 路径的一部分:

  1. [ApiController]
  2. public class Products2ApiController : ControllerBase
  3. {
  4. [HttpGet("/products2/{id}", Name = "Products_List")]
  5. public IActionResult GetProduct(int id)
  6. {
  7. return ControllerContext.MyDisplayRouteInfo(id);
  8. }
  9. }

Products2ApiController.GetProduct(int) 操作:

  • 以 URL 路径(如 /products2/3)运行
  • 不会 /products2URL 路径运行。

使用 [Consumes] 属性,操作可以限制支持的请求内容类型。有关详细信息,请参阅使用 "使用" 属性定义支持的请求内容类型

请参阅路由了解路由模板和相关选项的完整说明。

有关 [ApiController]的详细信息,请参阅ApiController 属性

路由名称Route name

下面的代码定义 Products_List的路由名称:

  1. [ApiController]
  2. public class Products2ApiController : ControllerBase
  3. {
  4. [HttpGet("/products2/{id}", Name = "Products_List")]
  5. public IActionResult GetProduct(int id)
  6. {
  7. return ControllerContext.MyDisplayRouteInfo(id);
  8. }
  9. }

可以使用路由名称基于特定路由生成 URL。路由名称:

  • 不会影响路由的 URL 匹配行为。
  • 仅用于生成 URL。

路由名称必须在应用程序范围内唯一。

对比前面的代码和传统的默认路由,将 id 参数定义为可选({id?})。精确指定 Api 的功能具有一些优点,例如允许 /products/products/5 调度到不同的操作。

组合特性路由Combining attribute routes

若要使属性路由减少重复,可将控制器上的路由属性与各个操作上的路由属性合并。控制器上定义的所有路由模板均作为操作上路由模板的前缀。在控制器上放置路由属性会使控制器中的所有操作都使用属性路由。

  1. [ApiController]
  2. [Route("products")]
  3. public class ProductsApiController : ControllerBase
  4. {
  5. [HttpGet]
  6. public IActionResult ListProducts()
  7. {
  8. return ControllerContext.MyDisplayRouteInfo();
  9. }
  10. [HttpGet("{id}")]
  11. public IActionResult GetProduct(int id)
  12. {
  13. return ControllerContext.MyDisplayRouteInfo(id);
  14. }
  15. }

在上面的示例中:

  • URL 路径 /products 可以匹配 ProductsApi.ListProducts
  • URL 路径 /products/5 可以匹配 ProductsApi.GetProduct(int)

这两个操作仅匹配 HTTP GET,因为它们用 [HttpGet] 属性进行标记。

应用于操作的以 /~/ 开头的路由模板不与应用于控制器的路由模板合并。下面的示例匹配一组类似于默认路由的 URL 路径。

  1. [Route("Home")]
  2. public class HomeController : Controller
  3. {
  4. [Route("")]
  5. [Route("Index")]
  6. [Route("/")]
  7. public IActionResult Index()
  8. {
  9. return ControllerContext.MyDisplayRouteInfo();
  10. }
  11. [Route("About")]
  12. public IActionResult About()
  13. {
  14. return ControllerContext.MyDisplayRouteInfo();
  15. }
  16. }

下表说明了上述代码中的 [Route] 属性:

属性[Route("Home")] 结合定义路由模板
[Route("")]"Home"
[Route("Index")]"Home/Index"
[Route("/")]""
[Route("About")]"Home/About"

属性路由顺序Attribute route order

路由构建树并同时匹配所有终结点:

  • 路由条目的行为方式与置于理想排序中的行为相同。
  • 最特定的路由在更通用的路由之前有机会执行。

例如,属性路由(如 blog/search/{topic})比 blog/{*article}等属性路由更为具体。默认情况下,blog/search/{topic} 路由具有较高的优先级,因为它更为具体。使用传统路由,开发人员负责按所需的顺序放置路由。

属性路由可以使用 Order 属性配置订单。提供的所有路由属性都包括 Order路由按 Order 属性的升序进行处理。默认顺序为 0使用 Order = -1 设置路由之前,在未设置顺序的路由之前运行。使用 Order = 1 设置路由将在默认路由排序之后运行。

避免根据 Order如果应用的 URL 空间需要显式顺序值才能正确路由,则很可能会使客户端混淆。通常,属性路由选择 URL 匹配的正确路由。如果用于 URL 生成的默认顺序不起作用,则使用路由名称作为替代通常比应用 Order 属性简单。

请考虑以下两个控制器,它们都定义了与 /home的路由匹配:

  1. public class HomeController : Controller
  2. {
  3. [Route("")]
  4. [Route("Home")]
  5. [Route("Home/Index")]
  6. [Route("Home/Index/{id?}")]
  7. public IActionResult Index(int? id)
  8. {
  9. return ControllerContext.MyDisplayRouteInfo(id);
  10. }
  11. [Route("Home/About")]
  12. [Route("Home/About/{id?}")]
  13. public IActionResult About(int? id)
  14. {
  15. return ControllerContext.MyDisplayRouteInfo(id);
  16. }
  17. }
  1. public class MyDemoController : Controller
  2. {
  3. [Route("")]
  4. [Route("Home")]
  5. [Route("Home/Index")]
  6. [Route("Home/Index/{id?}")]
  7. public IActionResult MyIndex(int? id)
  8. {
  9. return ControllerContext.MyDisplayRouteInfo(id);
  10. }
  11. [Route("Home/About")]
  12. [Route("Home/About/{id?}")]
  13. public IActionResult MyAbout(int? id)
  14. {
  15. return ControllerContext.MyDisplayRouteInfo(id);
  16. }
  17. }

使用前面的代码请求 /home 会引发异常,如下所示:

  1. AmbiguousMatchException: The request matched multiple endpoints. Matches:
  2. WebMvcRouting.Controllers.HomeController.Index
  3. WebMvcRouting.Controllers.MyDemoController.MyIndex

Order 添加到某个路由属性可解决歧义:

  1. [Route("")]
  2. [Route("Home", Order = 2)]
  3. [Route("Home/MyIndex")]
  4. public IActionResult MyIndex()
  5. {
  6. return ControllerContext.MyDisplayRouteInfo();
  7. }

在前面的代码中,/home 运行 HomeController.Index 终结点。若要转到 MyDemoController.MyIndex,请请求 /home/MyIndex说明

  • 上面的代码是一个示例或不良路由设计。它用于阐释 Order 属性。
  • Order 属性仅解析歧义,该模板无法匹配。删除 [Route("Home")] 模板会更好。

有关 Razor Pages 的路由顺序的信息,请参阅Razor Pages 路由和应用约定:路由顺序

在某些情况下,将返回具有不明确路由的 HTTP 500 错误。使用日志记录查看导致 AmbiguousMatchException的终结点。

路由模板中的标记替换 [控制器],[操作],[区域]Token replacement in route templates [controller], [action], [area]

为方便起见,特性路由支持为保留路由参数替换标记,方法是将令牌括在以下其中一项:

  • 方括号: []
  • 大括号: {}

标记 [action][area][controller] 替换为定义路由的操作的操作名称、区域名称和控制器名称的值:

  1. [Route("[controller]/[action]")]
  2. public class Products0Controller : Controller
  3. {
  4. [HttpGet]
  5. public IActionResult List()
  6. {
  7. return ControllerContext.MyDisplayRouteInfo();
  8. }
  9. [HttpGet("{id}")]
  10. public IActionResult Edit(int id)
  11. {
  12. return ControllerContext.MyDisplayRouteInfo(id);
  13. }
  14. }

在上述代码中:

  1. [HttpGet]
  2. public IActionResult List()
  3. {
  4. return ControllerContext.MyDisplayRouteInfo();
  5. }
  • 匹配 /Products0/List
  1. [HttpGet("{id}")]
  2. public IActionResult Edit(int id)
  3. {
  4. return ControllerContext.MyDisplayRouteInfo(id);
  5. }
  • 匹配 /Products0/Edit/{id}

标记替换发生在属性路由生成的最后一步。前面的示例与下面的代码具有相同的行为:

  1. public class Products20Controller : Controller
  2. {
  3. [HttpGet("[controller]/[action]")] // Matches '/Products20/List'
  4. public IActionResult List()
  5. {
  6. return ControllerContext.MyDisplayRouteInfo();
  7. }
  8. [HttpGet("[controller]/[action]/{id}")] // Matches '/Products20/Edit/{id}'
  9. public IActionResult Edit(int id)
  10. {
  11. return ControllerContext.MyDisplayRouteInfo(id);
  12. }
  13. }

如果要用英语以外的语言阅读此内容,并且你希望按你的母语查看代码注释,请在此 GitHub 讨论问题中告知我们。

属性路由还可以与继承结合使用。这与标记替换功能功能强大。标记替换也适用于属性路由定义的路由名称。[Route("[controller]/[action]", Name="[controller]_[action]")]为每个操作生成唯一的路由名称:

  1. [ApiController]
  2. [Route("api/[controller]/[action]", Name = "[controller]_[action]")]
  3. public abstract class MyBase2Controller : ControllerBase
  4. {
  5. }
  6. public class Products11Controller : MyBase2Controller
  7. {
  8. [HttpGet] // /api/products11/
  9. public IActionResult List()
  10. {
  11. return ControllerContext.MyDisplayRouteInfo();
  12. }
  13. [HttpGet("{id}")] // /api/products11/edit/3
  14. public IActionResult Edit(int id)
  15. {
  16. return ControllerContext.MyDisplayRouteInfo(id);
  17. }
  18. }

标记替换也适用于属性路由定义的路由名称。[Route("[controller]/[action]", Name="[controller]_[action]")]为每个操作生成唯一的路由名称。

若要匹配文本标记替换分隔符 [],可通过重复该字符([[]])对其进行转义。

使用参数转换程序自定义标记替换Use a parameter transformer to customize token replacement

使用参数转换程序可以自定义标记替换。参数转换程序实现 IOutboundParameterTransformer 并转换参数值。例如,自定义 SlugifyParameterTransformer 参数转换器将 SubscriptionManagement 路由值更改为 subscription-management

  1. public class SlugifyParameterTransformer : IOutboundParameterTransformer
  2. {
  3. public string TransformOutbound(object value)
  4. {
  5. if (value == null) { return null; }
  6. // Slugify value
  7. return Regex.Replace(value.ToString(),
  8. "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
  9. }
  10. }

RouteTokenTransformerConvention 是应用程序模型约定,可以:

  • 将参数转换程序应用到应用程序中的所有属性路由。
  • 在替换属性路由标记值时对其进行自定义。
  1. public class SubscriptionManagementController : Controller
  2. {
  3. [HttpGet("[controller]/[action]")]
  4. public IActionResult ListAll()
  5. {
  6. return ControllerContext.MyDisplayRouteInfo();
  7. }
  8. }

前面的 ListAll 方法匹配 /subscription-management/list-all

RouteTokenTransformerConventionConfigureServices 中注册为选项。

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddControllersWithViews(options =>
  4. {
  5. options.Conventions.Add(new RouteTokenTransformerConvention(
  6. new SlugifyParameterTransformer()));
  7. });
  8. }

有关信息区的定义,请参阅网站上的 MDN web 文档

多个属性路由Multiple attribute routes

属性路由支持定义多个访问同一操作的路由。最常见的用法是模拟默认传统路由的行为,如以下示例中所示:

  1. [Route("[controller]")]
  2. public class Products13Controller : Controller
  3. {
  4. [Route("")] // Matches 'Products13'
  5. [Route("Index")] // Matches 'Products13/Index'
  6. public IActionResult Index()
  7. {
  8. return ControllerContext.MyDisplayRouteInfo();
  9. }

在控制器上放置多个路由属性意味着每个属性都与操作方法上的每个路由属性结合:

  1. [Route("Store")]
  2. [Route("[controller]")]
  3. public class Products6Controller : Controller
  4. {
  5. [HttpPost("Buy")] // Matches 'Products6/Buy' and 'Store/Buy'
  6. [HttpPost("Checkout")] // Matches 'Products6/Checkout' and 'Store/Checkout'
  7. public IActionResult Buy()
  8. {
  9. return ControllerContext.MyDisplayRouteInfo();
  10. }
  11. }

所有HTTP 谓词路由约束都实现 IActionConstraint

当实现 IActionConstraint 的多个路由属性放置在一个操作上时:

  • 每个操作约束都与应用于控制器的路由模板结合。
  1. [Route("api/[controller]")]
  2. public class Products7Controller : ControllerBase
  3. {
  4. [HttpPut("Buy")] // Matches PUT 'api/Products7/Buy'
  5. [HttpPost("Checkout")] // Matches POST 'api/Products7/Checkout'
  6. public IActionResult Buy()
  7. {
  8. return ControllerContext.MyDisplayRouteInfo();
  9. }
  10. }

在操作中使用多个路由可能看起来非常有用,更好的做法是让应用程序的 URL 空间基本且定义完善。在需要时对操作使用多个路由,例如支持现有客户端。

指定属性路由的可选参数、默认值和约束Specifying attribute route optional parameters, default values, and constraints

属性路由支持使用与传统路由相同的内联语法,来指定可选参数、默认值和约束。

  1. public class Products14Controller : Controller
  2. {
  3. [HttpPost("product14/{id:int}")]
  4. public IActionResult ShowProduct(int id)
  5. {
  6. return ControllerContext.MyDisplayRouteInfo(id);
  7. }
  8. }

在上面的代码中,[HttpPost("product/{id:int}")] 应用路由约束。ProductsController.ShowProduct 操作仅通过 URL 路径(如 /product/3)进行匹配。路由模板部分 {id:int} 仅将该段限制为整数。

  1. public class HomeController : Controller
  2. {
  3. public IActionResult Index()
  4. {
  5. return ControllerContext.MyDisplayRouteInfo();
  6. }
  7. }

有关路由模板语法的详细说明,请参阅路由模板参考

使用 IRouteTemplateProvider 的自定义路由属性Custom route attributes using IRouteTemplateProvider

所有路由特性都实现 IRouteTemplateProviderASP.NET Core 运行时:

  • 应用启动时,在控制器类和操作方法上查找属性。
  • 使用实现 IRouteTemplateProvider 的特性来生成初始路由集。

实现 IRouteTemplateProvider 以定义自定义路由属性。每个 IRouteTemplateProvider 都允许定义一个包含自定义路由模板、顺序和名称的路由:

  1. public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
  2. {
  3. public string Template => "api/[controller]";
  4. public int? Order => 2;
  5. public string Name { get; set; }
  6. }
  7. [MyApiController]
  8. [ApiController]
  9. public class MyTestApiController : ControllerBase
  10. {
  11. // GET /api/MyTestApi
  12. [HttpGet]
  13. public IActionResult Get()
  14. {
  15. return ControllerContext.MyDisplayRouteInfo();
  16. }
  17. }

前面的 Get 方法返回 Order = 2, Template = api/MyTestApi

使用应用程序模型自定义属性路由Use application model to customize attribute routes

应用程序模型:

  • 是在启动时创建的对象模型。
  • 包含 ASP.NET Core 用来在应用程序中路由和执行操作的所有元数据。

应用程序模型包括从路由属性收集的所有数据。路由属性中的数据由 IRouteTemplateProvider 实现提供。规范

  • 可以编写来修改应用程序模型,以自定义路由的行为方式。
  • 在应用程序启动时读取。

本部分显示了使用应用程序模型自定义路由的基本示例。下面的代码使路由大致与项目的文件夹结构对齐。

  1. public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
  2. {
  3. private readonly string _baseNamespace;
  4. public NamespaceRoutingConvention(string baseNamespace)
  5. {
  6. _baseNamespace = baseNamespace;
  7. }
  8. public void Apply(ControllerModel controller)
  9. {
  10. var hasRouteAttributes = controller.Selectors.Any(selector =>
  11. selector.AttributeRouteModel != null);
  12. if (hasRouteAttributes)
  13. {
  14. return;
  15. }
  16. var namespc = controller.ControllerType.Namespace;
  17. if (namespc == null)
  18. return;
  19. var template = new StringBuilder();
  20. template.Append(namespc, _baseNamespace.Length + 1,
  21. namespc.Length - _baseNamespace.Length - 1);
  22. template.Replace('.', '/');
  23. template.Append("/[controller]/[action]/{id?}");
  24. foreach (var selector in controller.Selectors)
  25. {
  26. selector.AttributeRouteModel = new AttributeRouteModel()
  27. {
  28. Template = template.ToString()
  29. };
  30. }
  31. }
  32. }

下面的代码阻止将 namespace 约定应用到已路由属性的控制器:

  1. public void Apply(ControllerModel controller)
  2. {
  3. var hasRouteAttributes = controller.Selectors.Any(selector =>
  4. selector.AttributeRouteModel != null);
  5. if (hasRouteAttributes)
  6. {
  7. return;
  8. }

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

  1. [Route("[controller]/[action]/{id?}")]
  2. public class ManagersController : Controller
  3. {
  4. // /managers/index
  5. public IActionResult Index()
  6. {
  7. var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
  8. return Content($"Index- template:{template}");
  9. }
  10. public IActionResult List(int? id)
  11. {
  12. var path = Request.Path.Value;
  13. return Content($"List- Path:{path}");
  14. }
  15. }

NamespaceRoutingConvention.Apply 方法:

  • 如果控制器为属性路由,则不执行任何操作。
  • 基于 namespace设置控制器模板,并删除基本 namespace

可在 Startup.ConfigureServices中应用 NamespaceRoutingConvention

  1. namespace My.Application
  2. {
  3. public class Startup
  4. {
  5. public Startup(IConfiguration configuration)
  6. {
  7. Configuration = configuration;
  8. }
  9. public IConfiguration Configuration { get; }
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. services.AddControllersWithViews(options =>
  13. {
  14. options.Conventions.Add(
  15. new NamespaceRoutingConvention(typeof(Startup).Namespace));
  16. });
  17. }
  18. // Remaining code ommitted for brevity.

例如,请考虑以下控制器:

  1. using Microsoft.AspNetCore.Mvc;
  2. namespace My.Application.Admin.Controllers
  3. {
  4. public class UsersController : Controller
  5. {
  6. // GET /admin/controllers/users/index
  7. public IActionResult Index()
  8. {
  9. var fullname = typeof(UsersController).FullName;
  10. var template =
  11. ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
  12. var path = Request.Path.Value;
  13. return Content($"Path: {path} fullname: {fullname} template:{template}");
  14. }
  15. public IActionResult List(int? id)
  16. {
  17. var path = Request.Path.Value;
  18. return Content($"Path: {path} ID:{id}");
  19. }
  20. }
  21. }

在上述代码中:

  • 基本 namespaceMy.Application的。
  • 上述控制器的全名是 My.Application.Admin.Controllers.UsersController的。
  • NamespaceRoutingConvention 将控制器模板设置为 Admin/Controllers/Users/[action]/{id?

NamespaceRoutingConvention 还可以作为控制器上的属性应用:

  1. [NamespaceRoutingConvention("My.Application")]
  2. public class TestController : Controller
  3. {
  4. // /admin/controllers/test/index
  5. public IActionResult Index()
  6. {
  7. var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
  8. var actionname = ControllerContext.ActionDescriptor.ActionName;
  9. return Content($"Action- {actionname} template:{template}");
  10. }
  11. public IActionResult List(int? id)
  12. {
  13. var path = Request.Path.Value;
  14. return Content($"List- Path:{path}");
  15. }
  16. }

混合路由:属性路由与传统路由Mixed routing: Attribute routing vs conventional routing

ASP.NET Core 应用可以混合使用传统路由和属性路由。通常将传统路由用于为浏览器处理 HTML 页面的控制器,将属性路由用于处理 REST API 的控制器。

操作既支持传统路由,也支持属性路由。通过在控制器或操作上放置路由可实现属性路由。不能通过传统路由访问定义属性路由的操作,反之亦然。控制器上的任何路由属性都使控制器属性中的所有操作都已路由。

属性路由和传统路由使用相同的路由引擎。

URL 生成和环境值URL Generation and ambient values

应用可以使用路由 URL 生成功能来生成指向操作的 URL 链接。生成 Url 可消除硬编码 Url,使代码更可靠和更易于维护。本部分重点介绍 MVC 提供的 URL 生成功能,仅介绍 URL 生成的工作原理的基础知识。有关 URL 生成的详细说明,请参阅路由

IUrlHelper 接口是用于 URL 生成的 MVC 和路由之间基础结构的基础元素。通过 "控制器"、"视图" 和 "视图" 组件中的 "Url" 属性可获取 IUrlHelper 的实例。

在下面的示例中,通过 Controller.Url 属性使用 IUrlHelper 接口来生成另一个操作的 URL。

  1. public class UrlGenerationController : Controller
  2. {
  3. public IActionResult Source()
  4. {
  5. // Generates /UrlGeneration/Destination
  6. var url = Url.Action("Destination");
  7. return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
  8. }
  9. public IActionResult Destination()
  10. {
  11. return ControllerContext.MyDisplayRouteInfo();
  12. }
  13. }

如果应用使用默认传统路由,则 url 变量的值是 /UrlGeneration/Destination的 URL 路径字符串。此 URL 路径由路由通过组合创建:

  • 当前请求中的路由值,称为 "环境值"。
  • 传递给 Url.Action 的值,并将这些值替换为路由模板:
  1. ambient values: { controller = "UrlGeneration", action = "Source" }
  2. values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
  3. route template: {controller}/{action}/{id?}
  4. result: /UrlGeneration/Destination

路由模板中的每个路由参数都会通过将名称与这些值和环境值匹配,来替换掉原来的值。不具有值的路由参数可以:

  • 如果有一个默认值,则使用默认值。
  • 如果是可选的,则跳过它。例如,id 从路由模板 {controller}/{action}/{id?}

如果任何所需的路由参数没有对应的值,URL 生成将失败。如果某个路由的 URL 生成失败,则尝试下一个路由,直到尝试所有路由或找到匹配项为止。

Url.Action 前面的示例假定传统路由URL 生成的工作方式类似于属性路由,但概念不同。对于传统路由:

  • 路由值用于扩展模板。
  • controlleraction 的路由值通常出现在该模板中。这是因为由路由匹配的 Url 遵循约定。

下面的示例使用属性路由:

  1. public class UrlGenerationAttrController : Controller
  2. {
  3. [HttpGet("custom")]
  4. public IActionResult Source()
  5. {
  6. var url = Url.Action("Destination");
  7. return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
  8. }
  9. [HttpGet("custom/url/to/destination")]
  10. public IActionResult Destination()
  11. {
  12. return ControllerContext.MyDisplayRouteInfo();
  13. }
  14. }

前面代码中的 Source 操作生成 custom/url/to/destination

ASP.NET Core 3.0 中添加了 LinkGenerator 作为 IUrlHelper的替代项。LinkGenerator 提供类似但更灵活的功能。另外,IUrlHelper 上的方法还具有相应的 LinkGenerator 方法系列。

根据操作名称生成 URLGenerating URLs by action name

LinkGenerator、GetPathByAction和所有相关重载都旨在通过指定控制器名称和操作名称来生成目标终结点。

使用 Url.Action时,运行时将提供 controlleraction 的当前路由值:

  • controlleraction 的值都属于环境值和值。方法 Url.Action 始终使用 actioncontroller 的当前值,并生成路由到当前操作的 URL 路径。

路由尝试使用环境值中的值来填充生成 URL 时未提供的信息。请考虑使用 { a = Alice, b = Bob, c = Carol, d = David }的环境值 {a}/{b}/{c}/{d} 路由:

  • 路由具有足够的信息来生成 URL,无需任何其他值。
  • 路由具有足够的信息,因为所有路由参数都具有值。

如果添加了值 { d = Donovan }

  • { d = David } 将被忽略。
  • 生成的 URL 路径是 Alice/Bob/Carol/Donovan

警告: URL 路径是分层的。在前面的示例中,如果添加了值 { c = Cheryl }

  • { c = Carol, d = David } 的两个值都将被忽略。
  • 不再存在 d 的值,URL 生成将失败。
  • 若要生成 URL,必须指定 cd 所需的值。

你可能希望在默认路由 {controller}/{action}/{id?}遇到此问题。此问题在实践中很罕见,因为 Url.Action 始终显式指定 controlleraction 值。

多个Url 重载。操作使用路由值对象来提供除 controlleraction以外的路由参数的值。路由值对象经常与 id一起使用。例如 Url.Action("Buy", "Products", new { id = 17 })路由值对象:

  • 按约定通常是匿名类型的对象。
  • 可以是 IDictionary<>POCO)。

任何与路由参数不匹配的附加路由值都放在查询字符串中。

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

前面的代码生成 /Products/Buy/17?color=red

下面的代码生成一个绝对 URL:

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

若要创建绝对 URL,请使用以下项之一:

  • 接受 protocol的重载。例如,前面的代码。
  • 默认情况下, LinkGenerator生成绝对 uri。

按路由生成 UrlGenerate URLs by route

前面的代码演示了如何通过传入控制器和操作名称来生成 URL。IUrlHelper 还提供了RouteUrl系列方法。这些方法类似于Url. 操作,但它们不会将 controller action 的当前值复制到路由值。Url.RouteUrl最常见的用法:

  • 指定用于生成 URL 的路由名称。
  • 通常不指定控制器或操作名称。
  1. public class UrlGeneration2Controller : Controller
  2. {
  3. [HttpGet("")]
  4. public IActionResult Source()
  5. {
  6. var url = Url.RouteUrl("Destination_Route");
  7. return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
  8. }
  9. [HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
  10. public IActionResult Destination()
  11. {
  12. return ControllerContext.MyDisplayRouteInfo();
  13. }

以下 Razor 文件生成到 Destination_Route的 HTML 链接:

  1. <h1>Test Links</h1>
  2. <ul>
  3. <li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
  4. </ul>

以 HTML 和 Razor 生成 UrlGenerate URLs in HTML and Razor

IHtmlHelper 提供 HtmlHelper 方法html.beginformhtml.actionlink分别生成 <form><a> 元素。这些方法使用Url 操作方法来生成 url,并接受类似参数。Url.RouteUrl 的配套 HtmlHelperHtml.BeginRouteFormHtml.RouteLink,两者具有相似的功能。

TagHelper 通过 form TagHelper 和 <a> TagHelper 生成 URL。两者均通过 IUrlHelper 来实现。有关详细信息,请参阅窗体中的标记帮助程序。

在视图内,可通过 IUrlHelper 属性将 Url 用于前文未涵盖的任何临时 URL 生成。

操作结果中的 URL 生成URL generation in Action Results

前面的示例演示了如何在控制器中使用 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);
}

操作结果 RedirectToActionCreatedAtAction 的工厂方法会遵循 IUrlHelper上的方法的类似模式。

专用传统路由的特殊情况Special case for dedicated conventional routes

传统路由可以使用一种特殊的路由定义,称为专用的传统路由在下面的示例中,名为 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,并最终成功。

区域Areas

区域是一项 MVC 功能,用于将相关功能作为一个单独的组组织到一个组中:

  • 控制器操作的路由命名空间。
  • 视图的文件夹结构。

通过使用区域,应用可以有多个具有相同名称的控制器,只要它们具有不同的区域即可。通过向 areacontroller 添加另一个路由参数 action,可使用区域为路由创建层次结构。本部分讨论路由如何与区域交互。有关如何将区域与视图结合使用的详细信息,请参阅区域

下面的示例将 MVC 配置为使用默认传统路由,并为 areaBlog指定 area 路由:

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

在前面的代码中,调用 MapAreaControllerRoute 来创建 "blog_route"第二个参数 "Blog"是区域名称。

当匹配 URL 路径(如 /Manage/Users/AddUser)时,"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 通过为使用所提供的区域名称(本例中为 area)的 Blog 提供默认值和约束,来创建路由。默认值确保路由始终生成 { area = Blog, … },约束要求在生成 URL 时使用值 { area = Blog, … }

传统路由依赖于顺序。通常情况下,应将具有区域的路由置于更早的位置,因为它们比没有区域的路由更具体。

使用前面的示例,路由值 { 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}");
    }

操作定义Action definition

控制器上的公共方法(具有NonAction特性的方法除外)是操作。

示例代码Sample code

ASP.NET Core MVC 使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。路由在启动代码或属性中定义。路由描述应如何将 URL 路径与操作相匹配。它还用于在响应中生成送出的 URL(用于链接)。

操作既支持传统路由,也支持属性路由。通过在控制器或操作上放置路由可实现属性路由。有关详细信息,请参阅混合路由

本文档将介绍 MVC 与路由之间的交互,以及典型的 MVC 应用如何使用各种路由功能。有关高级路由的详细信息,请参阅路由

设置路由中间件Setting up Routing Middleware

Configure 方法中,可能会看到与下面类似的代码:

app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

在对 UseMvc 的调用中,MapRoute 用于创建单个路由,亦称 default 路由。大多数 MVC 应用使用带有模板的路由,与 default 路由类似。

路由模板 "{controller=Home}/{action=Index}/{id?}" 可以匹配诸如 /Products/Details/5 之类的 URL 路径,并通过对路径进行标记来提取路由值 { controller = Products, action = Details, id = 5 }MVC 将尝试查找名为 ProductsController 的控制器并运行 Details 操作:

public class ProductsController : Controller
{
   public IActionResult Details(int id) { ... }
}

请注意,在此示例中,当调用此操作时,模型绑定会使用值 id = 5id 参数设置为 5有关更多详细信息,请参阅模型绑定

使用 default 路由:

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

路由模板:

  • {controller=Home}Home 定义为默认 controller

  • {action=Index}Index 定义为默认 action

  • {id?}id 定义为可选参数

默认路由参数和可选路由参数不必包含在 URL 路径中进行匹配。有关路由模板语法的详细说明,请参阅路由模板参考

"{controller=Home}/{action=Index}/{id?}" 可以匹配 URL 路径 / 并生成路由值 { controller = Home, action = Index }controlleraction 的值使用默认值,id 不生成值,因为 URL 路径中没有相应的段。MVC 使用这些路由值选择 HomeControllerIndex 操作:

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

通过使用此控制器定义和路由模板,将对以下任意 URL 路径执行 HomeController.Index 操作:

  • /Home/Index/17

  • /Home/Index

  • /Home

  • /

简便方法 UseMvcWithDefaultRoute

app.UseMvcWithDefaultRoute();

可用于替换:

app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

UseMvcUseMvcWithDefaultRoute 可向中间件管道添加 RouterMiddleware 的实例。MVC 不直接与中间件交互,而是使用路由来处理请求。MVC 通过 MvcRouteHandler 实例连接到路由。UseMvc 内的代码与下面类似:

var routes = new RouteBuilder(app);

// Add connection to MVC, will be hooked up by calls to MapRoute.
routes.DefaultHandler = new MvcRouteHandler(...);

// Execute callback to register routes.
// routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

// Create route collection and add the middleware.
app.UseRouter(routes.Build());

UseMvc 不直接定义任何路由,它向 attribute 路由的路由集合添加占位符。重载 UseMvc(Action<IRouteBuilder>) 则允许用户添加自己的路由,并且还支持属性路由。UseMvc 及其所有变体都会为属性路由添加占位符:无论如何配置 UseMvc,属性路由始终可用。UseMvcWithDefaultRoute 定义默认路由并支持属性路由。属性路由部分提供了有关属性路由的更多详细信息。

传统路由Conventional routing

default 路由:

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

前面的代码是传统路由的示例。此样式称为传统路由,因为它建立了一个 URL 路径约定

  • 第一个路径段映射到控制器名称。
  • 第二个映射到操作名称。
  • 第三段用于可选 idid 映射到模型实体。

使用此 default 路由时,URL 路径 /Products/List 映射到 ProductsController.List 操作,/Blog/Article/17 映射到 BlogController.Article此映射基于控制器和操作名称,而不基于命名空间、源文件位置或方法参数。

提示

使用默认路由进行传统路由时,可快速生成应用程序,无需为所定义的每项操作提供一个新的 URL 模式。对于包含 CRUD 样式操作的应用程序,通过保持各控制器间 URL 的一致性,可帮助简化代码,使 UI 更易预测。

警告

路由模板将 id 定义为可选参数,意味着无需在 URL 中提供 ID 也可执行操作。从 URL 中省略 id 通常会导致模型绑定将它设置为 0,进而导致在数据库中找不到与 id == 0 匹配的实体。属性路由可以提供细化控制,使某些操作需要 ID,某些操作不需要 ID。按照惯例,当可选参数(比如 id)有可能在正确的用法中出现时,本文档将涵盖这些参数。

多个路由Multiple routes

通过添加对 UseMvc 的多次调用,可以在 MapRoute 内添加多个路由。这样做可以定义多个约定,或添加专用于特定操作的传统路由,比如:

app.UseMvc(routes =>
{
   routes.MapRoute("blog", "blog/{*article}",
            defaults: new { controller = "Blog", action = "Article" });
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

此处的 blog 路由是一个专用的传统路由,这表示它使用传统路由系统,但专用于特定的操作。由于 controlleraction 不会在路由模板中作为参数显示,它们只能有默认值,因此,此路由将始终映射到 BlogController.Article 操作。

路由集合中的路由会进行排序,并按添加顺序进行处理。因此,在此示例中,将先尝试 blog 路由,再尝试 default 路由。

备注

专用传统路由通常使用全部捕获路由参数(如 {*article})来捕获 URL 路径的其余部分。这会使某个路由变得“太贪婪”,也就是说,它会匹配用户想要使用其他路由来匹配的 URL。将“贪婪的”路由放在路由表中靠后的位置可解决此问题。

回退Fallback

在处理请求时,MVC 将验证路由值能否用于在应用程序中查找控制器和操作。如果路由值与任何操作都不匹配,则将该路由视为不匹配,并尝试下一个路由。这称为回退,其目的是简化传统路由重叠的情况。

区分操作Disambiguating actions

当通过路由匹配到两项操作时,MVC 必须进行区分,以选择“最佳”候选项,否则会引发异常。例如:

public class ProductsController : Controller
{
   public IActionResult Edit(int id) { ... }

   [HttpPost]
   public IActionResult Edit(int id, Product product) { ... }
}

此控制器定义了两项操作,这两项操作均与 URL 路径 /Products/Edit/17 和路由数据 { controller = Products, action = Edit, id = 17 } 匹配。这是 MVC 控制器的典型模式,其中 Edit(int) 显示用于编辑产品的表单,Edit(int, Product) 处理已发布的表单。为此,MVC 需要在请求为 HTTP Edit(int, Product) 时选择 POST,在 Http 谓词为任何其他内容时选择 Edit(int)

HttpPostAttribute ( [HttpPost] ) 是 IActionConstraint 的实现,它仅允许执行当 Http 谓词为 POST 时选择的操作。IActionConstraint 的存在使 Edit(int, Product) 成为比 Edit(int)“更好”的匹配项,因此会先尝试 Edit(int, Product)

只需在特殊化方案中编写自定义 IActionConstraint 实现,但务必了解 HttpPostAttribute 等属性的角色 — 为其他 Http 谓词定义了类似的属性。在传统路由中,当操作属于 show form -> submit form 工作流时通常使用相同的操作名称。在阅读了解 IActionConstraint 部分后,此模式的便利性将变得更加明显。

如果匹配多个路由,但 MVC 找不到“最佳”路由,则会引发 AmbiguousActionException

路由名称Route names

以下示例中的字符串 "blog""default" 都是路由名称:

app.UseMvc(routes =>
{
   routes.MapRoute("blog", "blog/{*article}",
               defaults: new { controller = "Blog", action = "Article" });
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

路由名称为路由提供一个逻辑名称,以便使用命名路由来生成 URL。路由排序会使 URL 生成复杂化,而这极大地简化了 URL 创建。路由名称必须在应用程序范围内唯一。

路由名称不影响请求的 URL 匹配或处理;它们仅用于 URL 生成。路由提供了有关 URL 生成(包括 MVC 特定帮助程序中的 URL 生成)的更多详细信息。

属性路由Attribute routing

属性路由使用一组属性将操作直接映射到路由模板。在下面的示例中,app.UseMvc(); 方法使用 Configure,不传递任何路由。HomeController 将匹配一组 URL,这组 URL 与默认路由 {controller=Home}/{action=Index}/{id?} 匹配的 URL 类似:

public class HomeController : Controller
{
   [Route("")]
   [Route("Home")]
   [Route("Home/Index")]
   public IActionResult Index()
   {
      return View();
   }
   [Route("Home/About")]
   public IActionResult About()
   {
      return View();
   }
   [Route("Home/Contact")]
   public IActionResult Contact()
   {
      return View();
   }
}

将针对任意 URL 路径 HomeController.Index()//Home 执行 /Home/Index 操作。

备注

此示例重点介绍属性路由与传统路由之间的主要编程差异。属性路由需要更多输入来指定路由;传统的默认路由处理路由的方式则更简洁。但是,属性路由允许(并需要)精确控制应用于每项操作的路由模板。

使用属性路由时,控制器名称和操作名称对于操作的选择没有影响。此示例匹配的 URL 与上一示例相同。

public class MyDemoController : Controller
{
   [Route("")]
   [Route("Home")]
   [Route("Home/Index")]
   public IActionResult MyIndex()
   {
      return View("Index");
   }
   [Route("Home/About")]
   public IActionResult MyAbout()
   {
      return View("About");
   }
   [Route("Home/Contact")]
   public IActionResult MyContact()
   {
      return View("Contact");
   }
}

备注

上述路由模板未定义 actionareacontroller 的路由参数。事实上,属性路由中不允许使用这些路由参数。由于路由模板已与某项操作关联,因此,分析 URL 中的操作名称毫无意义。

使用 Http[Verb] 属性的属性路由Attribute routing with Http[Verb] attributes

属性路由还可以使用 Http[Verb] 属性,比如 HttpPostAttribute所有这些属性都可采用路由模板。此示例展示与同一路由模板匹配的两项操作:

[HttpGet("/products")]
public IActionResult ListProducts()
{
   // ...
}

[HttpPost("/products")]
public IActionResult CreateProduct(...)
{
   // ...
}

对于诸如 /products 之类的 URL 路径,当 Http 谓词为 ProductsApi.ListProducts 时将执行 GET 操作,当 Http 谓词为 ProductsApi.CreateProduct 时将执行 POST属性路由首先将 URL 与路由属性定义的路由模板集进行匹配。一旦某个路由模板匹配,就会应用 IActionConstraint 约束来确定可以执行的操作。

提示

生成 REST API 时,很少会在操作方法上使用 [Route(…)]这是因为该操作将接受所有 HTTP 方法。建议使用更特定的 HttpVerbAttributes 来明确 API 所支持的操作。REST API 的客户端需要知道映射到特定逻辑操作的路径和 Http 谓词。

由于属性路由适用于特定操作,因此,使参数变成路由模板定义中的必需参数很简单。在此示例中,id 是 URL 路径中的必需参数。

public class ProductsApiController : Controller
{
   [HttpGet("/products/{id}", Name = "Products_List")]
   public IActionResult GetProduct(int id) { ... }
}

将针对诸如 ProductsApi.GetProduct(int)(而非 /products/3)之类的 URL 路径执行 /products 操作。请参阅路由了解路由模板和相关选项的完整说明。

路由名称Route Name

以下代码定义 路由名称Products_List

public class ProductsApiController : Controller
{
   [HttpGet("/products/{id}", Name = "Products_List")]
   public IActionResult GetProduct(int id) { ... }
}

可以使用路由名称基于特定路由生成 URL。路由名称不影响路由的 URL 匹配行为,仅用于生成 URL。路由名称必须在应用程序范围内唯一。

备注

这一点与传统的默认路由相反,后者将 id 参数定义为可选参数 ({id?})。这种精确指定 API 的功能可带来一些好处,比如允许将 /products/products/5 分派到不同的操作。

合并路由Combining routes

若要使属性路由减少重复,可将控制器上的路由属性与各个操作上的路由属性合并。控制器上定义的所有路由模板均作为操作上路由模板的前缀。在控制器上放置路由属性会使控制器中的所有操作都使用属性路由。

[Route("products")]
public class ProductsApiController : Controller
{
   [HttpGet]
   public IActionResult ListProducts() { ... }

   [HttpGet("{id}")]
   public ActionResult GetProduct(int id) { ... }
}

在此示例中,URL 路径 /products 可以匹配 ProductsApi.ListProducts,URL 路径 /products/5 可以匹配 ProductsApi.GetProduct(int)这两项操作仅匹配 HTTP GET,因为它们用 HttpGetAttribute 标记。

应用于操作的以 /~/ 开头的路由模板不与应用于控制器的路由模板合并。此示例匹配一组与默认路由类似的 URL 路径。

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]      // Combines to define the route template "Home"
    [Route("Index")] // Combines to define the route template "Home/Index"
    [Route("/")]     // Doesn't combine, defines the route template ""
    public IActionResult Index()
    {
        ViewData["Message"] = "Home index";
        var url = Url.Action("Index", "Home");
        ViewData["Message"] = "Home index" + "var url = Url.Action; =  " + url;
        return View();
    }

    [Route("About")] // Combines to define the route template "Home/About"
    public IActionResult About()
    {
        return View();
    }   
}

对属性路由排序Ordering attribute routes

与按定义的顺序执行的传统路由不同,属性路由会生成一个树并同时匹配所有路由。其行为就像路由条目是以理想排序方式放置的一样;最特定的路由有机会比较一般的路由先执行。

例如,像 blog/search/{topic} 这样的路由比像 blog/{*article} 这样的路由更特定。从逻辑上讲,blog/search/{topic} 路由默认情况下先“运行”,因为这是唯一合理的排序。使用传统路由时,开发人员负责按所需顺序放置路由。

属性路由可以使用框架提供的所有路由属性的 Order 属性来配置顺序。路由按 Order 属性的升序进行处理。默认顺序为 0使用 Order = -1 设置的路由比未设置顺序的路由先运行。使用 Order = 1 设置的路由在默认路由排序后运行。

提示

避免依赖 Order如果 URL 空间需要有显式顺序值才能正确进行路由,则同样可能使客户端混淆不清。属性路由通常选择与 URL 匹配的正确路由。如果用于 URL 生成的默认顺序不起作用,使用路由名称作为替代项通常比应用 Order 属性更简单。

Razor Pages 路由和 MVC 控制器路由共享一个实现。可在 路由和应用约定:路由顺序中找到有关 Razor Pages 主题中路由顺序的信息。

路由模板中的标记替换([controller]、[action]、[area])Token replacement in route templates ([controller], [action], [area])

为方便起见,属性路由支持标记替换,方法是将标记用大括号([])括起来。标记 [action][area][controller] 替换为定义了路由的操作中的操作名称值、区域名称值和控制器名称值。在接下来的示例中,操作与注释中所述的 URL 路径匹配:

[Route("[controller]/[action]")]
public class ProductsController : Controller
{
    [HttpGet] // Matches '/Products/List'
    public IActionResult List() {
        // ...
    }

    [HttpGet("{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) {
        // ...
    }
}

标记替换发生在属性路由生成的最后一步。上述示例的行为方式将与以下代码相同:


public class ProductsController : Controller
{
    [HttpGet("[controller]/[action]")] // Matches '/Products/List'
    public IActionResult List() {
        // ...
    }

    [HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) {
        // ...
    }
}

属性路由还可以与继承结合使用。与标记替换结合使用时尤为强大。

[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }

public class ProductsController : MyBaseController
{
   [HttpGet] // Matches '/api/Products'
   public IActionResult List() { ... }

   [HttpPut("{id}")] // Matches '/api/Products/{id}'
   public IActionResult Edit(int id) { ... }
}

标记替换也适用于属性路由定义的路由名称。[Route("[controller]/[action]", Name="[controller]_[action]")] 为每项操作生成一个唯一的路由名称。

若要匹配文本标记替换分隔符 [],可通过重复该字符([[]])对其进行转义。

使用参数转换程序自定义标记替换Use a parameter transformer to customize token replacement

使用参数转换程序可以自定义标记替换。参数转换程序实现 IOutboundParameterTransformer 并转换参数值。例如,一个自定义 SlugifyParameterTransformer 参数转换程序可将 SubscriptionManagement 路由值更改为 subscription-management

RouteTokenTransformerConvention 是应用程序模型约定,可以:

  • 将参数转换程序应用到应用程序中的所有属性路由。
  • 在替换属性路由标记值时对其进行自定义。
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")] // Matches '/subscription-management/list-all'
    public IActionResult ListAll() { ... }
}

RouteTokenTransformerConventionConfigureServices 中注册为选项。

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

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

        // Slugify value
        return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

多个路由Multiple Routes

属性路由支持定义多个访问同一操作的路由。此操作最常用于模拟默认传统路由的行为,如以下示例所示:

[Route("[controller]")]
public class ProductsController : Controller
{
   [Route("")]     // Matches 'Products'
   [Route("Index")] // Matches 'Products/Index'
   public IActionResult Index()
}

在控制器上放置多个路由属性意味着,每个路由属性将与操作方法上的每个路由属性合并。

[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
   [HttpPost("Buy")]     // Matches 'Products/Buy' and 'Store/Buy'
   [HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
   public IActionResult Buy()
}

当在某个操作上放置多个路由属性(可实现 IActionConstraint)时,每个操作约束将与定义它的属性中的路由模板合并。

[Route("api/[controller]")]
public class ProductsController : Controller
{
   [HttpPut("Buy")]      // Matches PUT 'api/Products/Buy'
   [HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'
   public IActionResult Buy()
}

提示

在操作上使用多个路由可能看起来很强大,但更建议使应用程序的 URL 空间保持简洁且定义完善。仅在需要时,例如为了支持现有客户端,才在操作上使用多个路由。

指定属性路由的可选参数、默认值和约束Specifying attribute route optional parameters, default values, and constraints

属性路由支持使用与传统路由相同的内联语法,来指定可选参数、默认值和约束。

[HttpPost("product/{id:int}")]
public IActionResult ShowProduct(int id)
{
   // ...
}

有关路由模板语法的详细说明,请参阅路由模板参考

使用 IRouteTemplateProvider 的自定义路由属性Custom route attributes using IRouteTemplateProvider

该框架中提供的所有路由属性([Route(…)][HttpGet(…)] 等)都可实现 IRouteTemplateProvider 接口。当应用启动时,MVC 会查找控制器类和操作方法上的属性,并使用可实现 IRouteTemplateProvider 的属性生成一组初始路由。

用户可以实现 IRouteTemplateProvider 来定义自己的路由属性。每个 IRouteTemplateProvider 都允许定义一个包含自定义路由模板、顺序和名称的路由:

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
   public string Template => "api/[controller]";

   public int? Order { get; set; }

   public string Name { get; set; }
}

应用 Template 时,上述示例中的属性会自动将 "api/[controller]" 设置为 [MyApiController]

使用应用程序模型自定义属性路由Using Application Model to customize attribute routes

应用程序模型是一个在启动时创建的对象模型,MVC 可使用其中的所有元数据来路由和执行操作。应用程序模型包含从路由属性收集(通过 IRouteTemplateProvider)的所有数据。可通过编写约定在启动时修改应用程序模型,以便自定义路由的行为方式。此部分通过一个简单的示例说明了如何使用应用程序模型自定义路由。

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;
using System.Text;
public class NamespaceRoutingConvention : 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)
        {
            // This controller manually defined some routes, so treat this 
            // as an override and not apply the convention here.
            return;
        }

        // Use the namespace and controller name to infer a route for the controller.
        //
        // Example:
        //
        //  controller.ControllerTypeInfo ->    "My.Application.Admin.UsersController"
        //  baseNamespace ->                    "My.Application"
        //
        //  template =>                         "Admin/[controller]"
        //
        // This makes your routes roughly line up with the folder structure of your project.
        //
        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]");

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

混合路由:属性路由与传统路由Mixed routing: Attribute routing vs conventional routing

MVC 应用程序可以混合使用传统路由与属性路由。通常将传统路由用于为浏览器处理 HTML 页面的控制器,将属性路由用于处理 REST API 的控制器。

操作既支持传统路由,也支持属性路由。通过在控制器或操作上放置路由可实现属性路由。不能通过传统路由访问定义属性路由的操作,反之亦然。控制器上的任何路由属性都会使控制器中的所有操作使用属性路由。

备注

这两种路由系统的区别在于 URL 与路由模板匹配后所应用的过程。在传统路由中,将使用匹配项中的路由值,从包含所有传统路由操作的查找表中选择操作和控制器。在属性路由中,每个模板都与某项操作关联,无需进行进一步的查找。

复杂段Complex segments

复杂段(例如,[Route("/dog{token}cat")])通过非贪婪的方式从右到左匹配文字进行处理。有关说明,请参阅源代码有关详细信息,请参阅此问题

URL 生成URL Generation

MVC 应用程序可以使用路由的 URL 生成功能,生成指向操作的 URL 链接。生成 URL 可消除硬编码 URL,使代码更稳定、更易维护。此部分重点介绍 MVC 提供的 URL 生成功能,并且仅涵盖 URL 生成工作原理的基础知识。有关 URL 生成的详细说明,请参阅路由

IUrlHelper 接口用于生成 URL,是 MVC 与路由之间的基础结构的基础部分。在控制器、视图和视图组件中,可通过 IUrlHelper 属性找到 Url 的实例。

在此示例中,将通过 IUrlHelper 属性使用 Controller.Url 接口来生成指向另一项操作的 URL。

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return Content($"Go check out {url}, it's really great.");
    }

    public IActionResult Destination()
    {
        return View();
    }
}

如果应用程序使用的是传统默认路由,则 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

路由模板中的每个路由参数都会通过将名称与这些值和环境值匹配,来替换掉原来的值。没有值的路由参数如果有默认值,则可使用默认值;如果本身是可选参数(比如此示例中的 id),则可直接跳过。如果任何所需路由参数没有对应的值,URL 生成将失败。如果某个路由的 URL 生成失败,则尝试下一个路由,直到尝试所有路由或找到匹配项为止。

上面的 Url.Action 示例假定使用传统路由,但 URL 生成功能的工作方式与属性路由相似,只不过概念不同。在传统路由中,路由值用于扩展模板,controlleraction 的路由值通常出现在该模板中 — 这种做法可行是因为通过路由匹配的 URL 遵守某项约定在属性路由中,controlleraction 的路由值不能出现在模板中,它们用于查找要使用的模板。

此示例使用属性路由:

// In Startup class
public void Configure(IApplicationBuilder app)
{
    app.UseMvc();
}
using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination"); // Generates /custom/url/to/destination
        return Content($"Go check out {url}, it's really great.");
    }

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

MVC 生成一个包含所有属性路由操作的查找表,并匹配 controlleraction 的值,以选择要用于生成 URL 的路由模板。在上述示例中,生成了 custom/url/to/destination

根据操作名称生成 URLGenerating URLs by action name

Url.Action (IUrlHelper .Action) 以及所有相关重载都基于这样一种想法:用户想通过指定控制器名称和操作名称来指定要链接的内容。

备注

使用 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 所需的值。使用默认路由 ({controller}/{action}/{id?}) 时可能会遇到此问题,但在实际操作中很少遇到此行为,因为 Url.Action 始终显式指定 controlleraction 值。

较长的 Url.Action 重载还采用附加路由值对象,为 controlleraction 以外的路由参数提供值。此重载最常与 id 结合使用,比如 Url.Action("Buy", "Products", new { id = 17 })按照惯例,路由值对象通常是匿名类型的对象,但它也可以是 IDictionary<>普通旧 .NET 对象任何与路由参数不匹配的附加路由值都放在查询字符串中。

using Microsoft.AspNetCore.Mvc;

public class TestController : Controller
{
    public IActionResult Index()
    {
        // Generates /Products/Buy/17?color=red
        var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
        return Content(url);
    }
}

提示

若要创建绝对 URL,请使用采用 protocol 的重载:Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)

根据路由生成 URLGenerating URLs by route

上面的代码演示了如何通过传入控制器和操作名称来生成 URL。IUrlHelper 还提供 Url.RouteUrl 系列的方法。这些方法类似于 Url.Action,但它们不会将 actioncontroller 的当前值复制到路由值。最常见的用法是指定一个路由名称,以使用特定路由来生成 URL,通常指定控制器或操作名称。

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destination
        return Content($"See {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination", Name = "Destination_Route")]
    public IActionResult Destination() {
        return View();
    }
}

在 HTML 中生成 URLGenerating URLs in HTML

IHtmlHelper 提供 HtmlHelper 方法 Html.BeginFormHtml.ActionLink,可分别生成 <form><a> 元素。这些方法使用 Url.Action 方法来生成 URL,并且采用相似的参数。Url.RouteUrl 的配套 HtmlHelperHtml.BeginRouteFormHtml.RouteLink,两者具有相似的功能。

TagHelper 通过 form TagHelper 和 <a> TagHelper 生成 URL。两者均通过 IUrlHelper 来实现。有关详细信息,请参阅使用表单

在视图内,可通过 IUrlHelper 属性将 Url 用于前文未涵盖的任何临时 URL 生成。

在操作结果中生成 URLGenerating URLS in Action Results

以上示例展示了如何在控制器中使用 IUrlHelper,不过,控制器中最常见的用法是将 URL 生成为操作结果的一部分。

ControllerBaseController 基类为操作结果提供简便的方法来引用另一项操作。一种典型用法是在接受用户输入后进行重定向。

public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        return RedirectToAction("Index");
    }
    return View(customer);
}

操作结果工厂方法遵循与 IUrlHelper 上的方法类似的模式。

专用传统路由的特殊情况Special case for dedicated conventional routes

传统路由可以使用一种特殊的路由定义,称为专用传统路由在下面的示例中,名为 blog 的路由是一种专用传统路由。

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}",
        defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

使用这些路由定义,Url.Action("Index", "Home") 将通过 / 路由生成 URL 路径 default,但是,为什么会这样?用户可能认为使用 { 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,并最终成功。

区域Areas

区域是一种 MVC 功能,用于将相关功能整理到一个组中,作为单独的路由命名空间(用于控制器操作)和文件夹结构(用于视图)。通过使用区域,应用程序可以有多个名称相同的控制器,只要它们具有不同的区域通过向 areacontroller 添加另一个路由参数 action,可使用区域为路由创建层次结构。此部分将讨论路由如何与区域交互;有关如何将区域与视图结合使用的详细信息,请参阅区域

下面的示例将 MVC 配置为使用默认传统路由和区域路由(用于名为 Blog 的区域):

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

与 URL 路径(比如 /Manage/Users/AddUser)匹配时,第一个路由将生成路由值 { area = Blog, controller = Users, action = AddUser }area 路由值由 area 的默认值生成,事实上,通过 MapAreaRoute 创建的路由等效于以下路由:

MapAreaRoute 通过为使用所提供的区域名称(本例中为 area)的 Blog 提供默认值和约束,来创建路由。默认值确保路由始终生成 { area = Blog, … },约束要求在生成 URL 时使用值 { area = Blog, … }

提示

传统路由依赖于顺序。一般情况下,具有区域的路由应放在路由表中靠前的位置,因为它们比没有区域的路由更特定。

在上面的示例中,路由值将与以下操作匹配:

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

AreaAttribute 用于将控制器表示为某个区域的一部分,比方说,此控制器位于 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 生成中具有粘性,如以下示例所示。

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

了解 IActionConstraintUnderstanding IActionConstraint

备注

此部分深入介绍框架内部结构以及 MVC 如何选择要执行的操作。典型的应用程序不需要自定义 IActionConstraint

即使不熟悉 IActionConstraint,也可能已经用过该接口。[HttpGet] 属性和类似的 [Http-VERB] 属性可实现 IActionConstraint 来限制操作方法的执行。

public class ProductsController : Controller
{
    [HttpGet]
    public IActionResult Edit() { }

    public IActionResult Edit(...) { }
}

假定使用默认传统路由,URL 路径 /Products/Edit 将生成值 { controller = Products, action = Edit },这将匹配此处所示的两项操作。IActionConstraint 术语中,我们会说,这两项操作都被视为候选项,因为它们都与该路由数据匹配。

HttpGetAttribute 执行时,它认为 Edit()GET 的匹配项,而不是任何其他 Http 谓词的匹配项。Edit(…) 操作未定义任何约束,因此将匹配任何 Http 谓词。因此,假定 Http 谓词为 POST,则仅 Edit(…) 匹配。不过,对于 GET,这两项操作仍然都能匹配,只是具有 IActionConstraint 的操作始终被认为比没有该接口的操作更匹配因此,由于 Edit() 具有 [HttpGet],则认为它更特定,在两项操作都能匹配的情况将选择它。

从概念上讲,IActionConstraint 是一种重载形式,但它并不重载具有相同名称的方法,而在匹配相同 URL 的操作之间重载。属性路由也使用 IActionConstraint,这可能会导致将不同控制器中的操作都视为候选项。

实现 IActionConstraintImplementing IActionConstraint

实现 IActionConstraint 最简单的方法是创建派生自 System.Attribute 的类,并将其置于操作和控制器上。MVC 将自动发现任何应用为属性的 IActionConstraint可使用应用程序模型应用约束,这可能是最灵活的一种方法,因为它允许对其应用方式进行元编程。

在下面的示例中,约束基于路由数据中的国家/地区代码选择操作。GitHub 上的完整示例

public class CountrySpecificAttribute : Attribute, IActionConstraint
{
    private readonly string _countryCode;

    public CountrySpecificAttribute(string countryCode)
    {
        _countryCode = countryCode;
    }

    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        return string.Equals(
            context.RouteContext.RouteData.Values["country"].ToString(),
            _countryCode,
            StringComparison.OrdinalIgnoreCase);
    }
}

用户负责实现 Accept 方法,并为要执行的约束选择“顺序”。在此例中,当 Accept 路由值匹配时,true 方法返回 country 以表示该操作是匹配项。它与 RouteValueAttribute 的不同之处在于,它允许回退到非属性化操作。通过该示例可以了解到,如果定义 en-US 操作,则像 fr-FR 这样的国家/地区代码将回退到一个未应用 [CountrySpecific(…)] 的较通用的控制器。

Order 属性决定约束所属的阶段操作约束基于 Order 分组运行。例如,该框架提供的所有 HTTP 方法属性均使用相同的 Order 值,以便在相同的阶段运行。用户可以按需设置阶段数来实现所需的策略。

提示

若要确定 Order 的值,请考虑是否应在 HTTP 方法前应用约束。数值较低的先运行。