ASP.NET Core 中路由到控制器操作

作者:Ryan NowakKirk LarkinnRick Anderson

Note

此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 10 版本

Warning

不再支持此版本的 ASP.NET Core。 有关详细信息,请参阅 .NET 和 .NET 核心支持策略。 有关当前版本,请参阅本文的 .NET 10 版本

ASP.NET Core控制器使用 Routing middleware 匹配传入请求的 URL,并将其映射到 actions。 路由模板:

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

操作既支持传统路由,也支持属性路由。 在控制器或 操作 上定义路由,就会使其成为属性路由。 有关详细信息,请参阅混合路由

本文档:

  • 说明了 MVC 与路由之间的交互:
    • 典型的 MVC 应用如何使用路由功能。
    • 涵盖两者:
    • 有关高级路由的详细信息,请参阅路由
  • 将默认路由系统称为终结点路由。 可以将控制器与以前版本的路由一起使用,以实现兼容性。 有关说明,请参阅 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 路由。 大多数具有控制器和视图的应用都使用类似 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);
        }
    }
    

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

使用前面的控制器定义和路由模板,操作 HomeController.Index 针对以下 URL 路径运行:

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

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

简便方法 MapDefaultControllerRoute

app.MapDefaultControllerRoute();

Replaces:

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

Important

路由是使用 UseRoutingUseEndpoints 中间件配置的。 要使用控制器,请执行以下操作:

应用通常不需要调用 UseRoutingUseEndpointsWebApplicationBuilder配置一个中间件管道,该管道使用Program.csUseRouting来包装在UseEndpoints中添加的中间件。 有关详细信息,请参阅 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 更具可预测性。

Warning

路由模板将 id 定义为可选。 无需在 URL 中提供可选 ID 即可执行操作。 通常,当 URL 中省略 id 时:

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

属性路由可以提供细化控制,使某些操作需要 ID,某些操作不需要 ID。 按照约定,当可选参数(比如 id)有可能在正确的用法中出现时,本文档将涵盖这些参数。

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

  • 支持基本的描述性路由方案。
  • 是基于 UI 的应用的有用起点。
  • 是许多 Web UI 应用所需的唯一路由模板。 对于较大的 Web UI 应用,使用 Areas 的另一种方式通常就足够了。

MapControllerRouteMapAreaRoute

  • 根据调用顺序,自动为它们的终结点分配 order 值。

ASP.NET Core中的终结点路由:

  • 没有路由概念。
  • 不提供扩展功能执行顺序的保证。 同时处理所有终结点。

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

属性路由将在本文档后面介绍。

多个传统路由

可以通过添加更多对MapControllerRoute的调用来配置多个MapAreaControllerRoute。 添加这些调用时,可以定义多个约定或添加专用于特定 操作的传统路由,例如:

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

前面代码中的 blog 路由是专用传统路由。 这是一个专用的传统路由,因为:

因为路由模板 "blog/{*article}" 不包含 controlleraction 作为参数:

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

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

在上面的示例中:

  • 由于你先添加了 blog 路由,因此它在匹配时的优先级高于 default 路由。
  • 这是 Slug 样式路由的一个示例,其中通常将项目名称作为 URL 的一部分。

Warning

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

  • 定义一个称为路由的概念。 UseRouting 向中间件管道添加路由匹配。 UseRouting 中间件查看应用中定义的终结点集,并根据请求选择最佳终结点匹配。
  • 提供有关扩展性的执行顺序保证,例如 IRouteConstraintIActionConstraint

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

传统路由顺序

传统路由仅匹配应用程序定义的动作和控制器的组合。 此方法简化了传统路由重叠的情况。 使用 MapControllerRouteMapDefaultControllerRouteMapAreaControllerRoute 添加路由时,这些终结点会根据调用这些方法的先后顺序自动获得顺序值。 来自列表中前面显示的路由的匹配优先级更高。 常规路由取决于顺序。 一般情况下,将有区域的路线放在前面,因为它们比没有区域的路线更具体。 使用全捕获路由参数的专用常规路由(例如 {*article})可能会使路由的匹配范围过大。 贪心路由会匹配那些你本来希望由其他路由来匹配的 URL。 将贪婪路由放在路由表的后面以防止贪婪匹配。

Warning

catch-all 参数可能由于路由中的 bug 无法正确匹配相应路由。 受此 Bug 影响的应用具有以下特征:

  • “通配路由”,例如 {**slug}"
  • “全匹配”路由未能匹配本应匹配的请求。
  • 删除其他路由会启动“全捕获”路由的功能。

有关出现此 bug 的示例情况,请参阅 GitHub 错误 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 Edit(int, Product) 时,选择 POST
  • Edit(int) 为其他任意值时,选择 Edit(int) 通常通过 GET 调用。

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

了解 HttpPostAttribute 等属性的作用很重要。 为其他 HTTP 谓词定义了类似的属性。 在 传统路由中,当操作属于“显示表单、提交表单”工作流的一部分时,通常会使用相同的操作名称。 例如,请参阅检查两个 Edit 操作方法

如果路由无法选择最佳候选终结点,则会引发一个 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 页面时,以下关键字是保留的路由参数名称:

  • 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 页面或控制器。

在 Razor 视图或 Razor 页面的上下文中保留以下关键字:

  • 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 Not Found 错误。
      [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

  • MyProductsController.ListProducts 操作在 HTTP 谓词GET 时运行。
  • MyProductsController.CreateProduct 操作在 HTTP 谓词POST 时运行。

生成 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) 操作:

  • 使用类似 /products2/3 的 URL 路径运行
  • 并非使用 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)

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

应用于操作且以 /~/ 开头的路由模板,不会与应用于控制器的路由模板组合。 以下示例匹配一组与默认路由类似的 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] 上述代码中的属性。

Attribute 可与 [Route("Home")] 结合 定义路由模板
[Route("")] Yes "Home"
[Route("Index")] Yes "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 文档

Warning

如果使用 System.Text.RegularExpressions 处理不受信任的输入,则设置一个超时时间。 恶意用户可能会向 RegularExpressions 提供输入,从而导致拒绝服务攻击。 使用 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 操作仅与 /product14/3 等 URL 路径匹配。 路由模板部分 {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 实现提供。 Conventions:

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

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

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

NamespaceRoutingConvention 可应用于 Program.cs

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 页面的控制器使用常规路由,并为提供 API 的 REST 控制器使用属性路由。

操作既支持传统路由,也支持属性路由。 在控制器或动作上放置路由时,它将变为属性路由。 无法通过传统路由访问由属性路由定义的操作,反之亦然,属性路由也不能访问由传统路由定义的操作。 任一控制器上的路由特性都会使该控制器中的所有操作都采用特性路由。

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

使用特殊字符进行路由

使用特殊字符进行路由可能会导致意外的结果。 例如,假设控制器具有以下操作方法:

[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 Encoded
/ %2F
+

路由参数并不总是 URL 解码的。 此问题可能会在未来得到解决。 有关详细信息,请参阅 以下GitHub问题;

URL 生成和环境值

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

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

在以下示例中,将通过 IUrlHelper 属性使用 Controller.Url 接口来生成指向另一项操作的 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

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

  • 使用默认值(如果有)。
  • 如果是可选的,可以跳过。 例如,路由模板 id 中的 {controller}/{action}/{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

LinkGenerator作为 IUrlHelper 的替代方法添加到 ASP.NET Core 3.0 中。 LinkGenerator 提供类似但更灵活的功能。 IUrlHelper 上的每个方法在 LinkGenerator 上也有一系列相应的方法。

根据操作名称生成 URL

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

使用 Url.Action时,运行时提供当前路由值 controller 以及 action

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

TagHelpers 通过 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);
}

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")使用/路由生成 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 配置为使用默认约定路由,以及一个用于名为 areaareaBlog 路由:

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 使用提供的区域名称(在本例中为 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.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。 在环境中 Development ,将日志级别设置为 appsettings.Development.json

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

ASP.NET Core控制器使用 Routing middleware 匹配传入请求的 URL,并将其映射到 actions。 路由模板:

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

操作既支持传统路由,也支持属性路由。 在控制器或 操作 上定义路由,就会使其成为属性路由。 有关详细信息,请参阅混合路由

本文档:

  • 说明了 MVC 与路由之间的交互:
    • 典型的 MVC 应用如何使用路由功能。
    • 涵盖两者:
    • 有关高级路由的详细信息,请参阅路由
  • 指在 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 路由。 大多数具有控制器和视图的应用都使用类似 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);
        }
    }
    

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

使用前面的控制器定义和路由模板,操作 HomeController.Index 针对以下 URL 路径运行:

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

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

简便方法 MapDefaultControllerRoute

endpoints.MapDefaultControllerRoute();

Replaces:

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

Important

路由是使用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 更具可预测性。

Warning

路由模板将 id 定义为可选。 无需在 URL 中提供可选 ID 即可执行操作。 通常,当 URL 中省略 id 时:

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

属性路由可以提供细化控制,使某些操作需要 ID,某些操作不需要 ID。 按照约定,当可选参数(比如 id)有可能在正确的用法中出现时,本文档将涵盖这些参数。

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

  • 支持基本的描述性路由方案。
  • 是基于 UI 的应用的有用起点。
  • 是许多 Web UI 应用所需的唯一路由模板。 对于较大的 Web UI 应用,使用 Areas 的另一种方式通常就足够了。

MapControllerRouteMapAreaRoute

  • 根据调用顺序,自动为它们的终结点分配 order 值。

ASP.NET Core 3.0 或更高版本中的终结点路由:

  • 没有路由概念。
  • 不提供扩展功能执行顺序的保证。 同时处理所有终结点。

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

属性路由将在本文档后面介绍。

多个传统路由

通过增加对 UseEndpoints 的调用,可以在 MapControllerRoute 内部添加多个 MapAreaControllerRoute。 这样做允许定义多个约定,或添加专用于特定操作的传统路由,例如:

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 路由是专用传统路由。 这是一个专用的传统路由,因为:

因为路由模板 "blog/{*article}" 不包含 controlleraction 作为参数:

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

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

在上面的示例中:

  • 由于你先添加了 blog 路由,因此它在匹配时的优先级高于 default 路由。
  • 这是 Slug 样式路由的一个示例,其中通常将项目名称作为 URL 的一部分。

Warning

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

  • 定义一个称为路由的概念。 UseRouting 向中间件管道添加路由匹配。 UseRouting 中间件查看应用中定义的终结点集,并根据请求选择最佳终结点匹配。
  • 提供有关扩展性的执行顺序保证,例如 IRouteConstraintIActionConstraint

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

传统路由顺序

传统路由仅匹配应用程序定义的动作和控制器的组合。 此方法简化了传统路由重叠的情况。 使用 MapControllerRouteMapDefaultControllerRouteMapAreaControllerRoute 添加路由时,这些终结点会根据调用这些方法的先后顺序自动获得顺序值。 来自列表中前面显示的路由的匹配优先级更高。 常规路由取决于顺序。 一般情况下,将有区域的路线放在前面,因为它们比没有区域的路线更具体。 使用全捕获路由参数的专用常规路由(例如 {*article})可能会使路由的匹配范围过大。 贪心路由会匹配那些你本来希望由其他路由来匹配的 URL。 将贪婪路由放在路由表的后面以防止贪婪匹配。

Warning

catch-all 参数可能由于路由中的 bug 无法正确匹配相应路由。 受此 Bug 影响的应用具有以下特征:

  • “通配路由”,例如 {**slug}"
  • “全匹配”路由未能匹配本应匹配的请求。
  • 删除其他路由会启动“全捕获”路由的功能。

有关出现此 bug 的示例情况,请参阅 GitHub 错误 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 Edit(int, Product) 时,选择 POST
  • Edit(int) 为其他任意值时,选择 Edit(int) 通常通过 GET 调用。

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

了解 HttpPostAttribute 等属性的作用很重要。 为其他 HTTP 谓词定义了类似的属性。 在传统路由中,当操作属于显示表单、提交表单工作流时,通常使用相同的操作名称。 例如,请参阅检查两个 Edit 操作方法

如果路由无法选择最佳候选终结点,则会引发一个 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();
    });
}

前面的代码通过在 MapControllers 中调用 UseEndpoints 来映射属性路由控制器。

在以下示例中:

  • 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 页面时,以下关键字是保留的路由参数名称:

  • 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 页面或控制器。

在 Razor 视图或 Razor 页面的上下文中保留以下关键字:

  • 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 Not Found 错误。
      [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

  • MyProductsController.ListProducts 操作在 HTTP 谓词GET 时运行。
  • MyProductsController.CreateProduct 操作在 HTTP 谓词POST 时运行。

生成 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) 操作:

  • 使用类似 /products2/3 的 URL 路径运行
  • 并非使用 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)

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

应用于操作且以 /~/ 开头的路由模板,不会与应用于控制器的路由模板组合。 以下示例匹配一组与默认路由类似的 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] 上述代码中的属性。

Attribute 可与 [Route("Home")] 结合 定义路由模板
[Route("")] Yes "Home"
[Route("Index")] Yes "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

RouteTokenTransformerConventionConfigureServices 中注册为选项。

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

有关 slug 的定义,请参阅 Slug 上的 MDN Web 文档

Warning

如果使用 System.Text.RegularExpressions 处理不受信任的输入,则设置一个超时时间。 恶意用户可能会向 RegularExpressions 提供输入,从而导致拒绝服务攻击。 使用 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 操作仅与 /product14/3 等 URL 路径匹配。 路由模板部分 {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 实现提供。 Conventions:

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

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

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

NamespaceRoutingConvention 可应用于 Startup.ConfigureServices

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 页面的控制器使用常规路由,并为提供 API 的 REST 控制器使用属性路由。

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

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

URL 生成和环境值

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

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

在以下示例中,将通过 IUrlHelper 属性使用 Controller.Url 接口来生成指向另一项操作的 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

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

  • 使用默认值(如果有)。
  • 如果是可选的,可以跳过。 例如,路由模板 id 中的 {controller}/{action}/{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

LinkGenerator作为 IUrlHelper 的替代方法添加到 ASP.NET Core 3.0 中。 LinkGenerator 提供类似但更灵活的功能。 IUrlHelper 上的每个方法在 LinkGenerator 上也有一系列相应的方法。

根据操作名称生成 URL

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

使用 Url.Action时,运行时提供当前路由值 controller 以及 action

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

TagHelpers 通过 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);
}

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")使用/路由生成 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 配置为使用默认约定路由,以及一个用于名为 areaareaBlog 路由:

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 使用提供的区域名称(在本例中为 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}");
    }

动作定义

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

示例代码

调试诊断

要获取详细的路由诊断输出,请将 Logging:LogLevel:Microsoft 设置为 Debug。 在环境中 Development ,将日志级别设置为 appsettings.Development.json

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