控制器操作的路由
原文:Routing to Controller Actions
作者:Ryan Nowak、Rick Anderson
翻译:娄宇(Lyrics)
校对:何镇汐、姚阿勇(Dr.Yao)
ASP.NET Core MVC 使用路由 中间件 来匹配传入请求的 URL 并映射到具体的操作。路由通过启动代码或者特性定义。路由描述 URL 路径应该如何匹配到操作。路由也同样用于生成响应中返回的 URL(用于链接)。
这篇文章将解释 MVC 和路由之间的相互作用,以及典型的 MVC 应用程序如何使用路由特性。查看 路由 获取更多高级路由信息。
配置路由中间件
在你的 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
并会通过标记路径提取路由值 { controller = Products, action = Details, id = 5 }
。MVC 将尝试定位名为 ProductsController
的控制器并运行操作 Details
:
public class ProductsController : Controller
{
public IActionResult Details(int id) { ... }
}
注意这个例子,当调用这个操作时,模型绑定会使用 id = 5
的值来将 id
参数设置为 5
。查看 模型绑定 获取更多信息。
使用 default
路由:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
路由模板:
{controller=Home}
定义Home
作为默认的controller
{action=Index}
定义Index
作为默认的action
{id?}
定义id
为可选项
默认和可选路由参数不需要出现在 URL 路径,查看 Routing 获取路由模板语法的详细描述。
"{controller=Home}/{action=Index}/{id?}"
可以匹配 URL 路径 /
并产生路由值 { controller = Home, action = Index }
。 controller
和 action
使用默认值,因为在 URL 路径中没有响应的片段,所以 id
不会产生值。MVC会使用这些路由值选择 HomeController
和 Index
操作:
public class HomeController : Controller
{
public IActionResult Index() { ... }
}
使用这个控制器和路由模板, HomeController.Index
操作会被以下任一 URL 路径执行:
/Home/Index/17
/Home/Index
/Home
/
简便的方法 UseMvcWithDefaultRoute:
app.UseMvcWithDefaultRoute();
可以被替换为:
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
UseMvc
和 UseMvcWithDefaultRoute
添加一个 RouterMiddleware 的实例到中间件管道。MVC 不直接与中间件交互,使用路由来处理请求。MVC 通过 MvcRouteHandler 的实例连接到路由。UseMvc
中的代码类似于下面:
var routes = new RouteBuilder(app);
// 添加连接到 MVC,将通过调用 MapRoute 连接。
routes.DefaultHandler = new MvcRouteHandler(...);
// 执行回调来注册路由。
// routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
// 创建路由集合并添加中间件。
app.UseRouter(routes.Build());
UseMvc 不会直接定义任何路由,它为 特性
路由在路由集合中添加了一个占位符。UseMvc(Action<IRouteBuilder>)
这个重载让你添加自己的路由并且也支持特性路由。UseMvc
和它所有的重载都为特性路由添加占位符,不管你如何配置 UseMvc
,特性路由总是可用的。 UseMvcWithDefaultRoute 定义一个默认路由并支持特性路由。
特性路由 章节包含了特性路由的信息。
常规路由
default
路由:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
是一个 常规路由 的例子。我们将这种风格称为 常规路由 因为它为 URL 路径建立了一个 约定 :
- 第一个路径片段映射控制器名。
- 第二个片段映射操作名。
- 第三个片段是一个可选的
id
用于映射到模型实体。
使用这个 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
。
多路由
你可以在 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
路由在这里是一个 专用常规路由,意味着它使用常规路由系统,但是专用于一个特殊的操作。由于 controller
和 action
不会作为参数出现在路由模板中,它们只能拥有默认值,因此这个路由将总是映射到操作 BlogController.Article
。
路由在路由集合中是有序的,并将按照它们添加的顺序处理。所以在这个例子中,blog
路由会在 default
路由之前尝试。
注解
专用常规路由 通常捕捉所有参数,比如使用{*article}
捕捉 URL 路径的剩余部分。这样使得路由 '太贪婪',这意味着它将匹配所有你打算与其他路由规则匹配的路由。把 'greedy' 路由在路由表中置后来解决这个问题。
回退
作为请求处理的一部分,MVC 将验证路由值是否可以用来在你的应用程序中找到控制器和操作。如果路由值不匹配任何操作,则不会认为路由匹配成功,将会尝试下一个路由。这叫做 回退,它的目的是简化路由重叠的情况。
消除歧义操作
当两个操作通过路由匹配,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 POST
时选择 Edit(int, Product)
,并在其他 HTTP 谓词时选择 Edit(int)
。
HttpPostAttribute ( [HttpPost]
) 是 IActionConstraint 的一个实现,它仅允许 HTTP 谓词为 POST
的请求访问操作。IActionConstraint
的存在使得 Edit(int, Product)
比 Edit(int)
更好匹配,所以会先首先尝试 Edit(int, Product)
。查看 理解 IActionConstraint 获取更多信息。
你只会在专门的场景才需要编写自定义的 IActionConstraint
实现,但重要的是要理解特性的作用,比如 HttpPostAttribute
—— 以及为其他 HTTP 谓词定义的类似的特性。在常规路由中,当操作是“显示表单 -> 提交表单”工作流时,操作使用相同的名字是很常见的。在回顾 URL 的生成 章节后,这种模式的方便将变得更加明显。
如果多个路由都匹配,并且 MVC 不能找到‘最好的’路由,将会抛出一个 AmbiguousActionException异常。
路由名称
在下面例子中的 "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。
特性路由
特性路由使用一组特性来直接将操作映射到路由模板。在下面的例子中,在 Configure
中使用 app.UseMvc();
且没有传入路由。HomeController
会匹配一组类似于 {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();
}
}
HomeController.Index()
操作会被 /
、/Home
或者 /Home/Index
中任一 URL 路径执行。
注解
这个例子突出了特性路由与常规路由一个关键的不同之处。特性路由需要更多的输入来指定一个路由;常规路由处理路由更加的简洁。然而,特性路由允许(也必须)精确控制每个操作的路由模板。
控制器名和操作名在特性路由中是 不会 影响选择哪个操作的。这个例子会匹配与上个例子相同的 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");
}
}
注解
上面的路由模板没有定义针对action
、area
以及controller
的路由参数。实际上,这些参数不允许出现在特性路由中。因为路由模板已经关联了一个操作,解析 URL 中的操作名是没有意义的。
特性路由也可以使用 HTTP[Verb]
特性,比如 HttpPostAttribute。所有这些特性都可以接受路由模板。这个例子展示两个操作匹配同一个路由模板:
[HttpGet("/products")]
public IActionResult ListProducts()
{
// ...
}
[HttpPost("/products")]
public IActionResult CreateProduct(...)
{
// ...
}
对于 /products
这个 URL 路径来说,ProductsApi.ListProducts
操作会在 HTTP 谓词是 GET
时执行,ProductsApi.CreateProduct
会在 HTTP 谓词是 POST
时执行。特性路由首先匹配路由模板集合中通过路由特性定义的 URL。一旦路由模板匹配,IActionConstraint 约束会应用与决定执行哪个操作。
小技巧
当构建一个 REST API,你几乎不会想在操作方法上使用[Route(...)]
。最好是使用更加具体的Http*Verb*Attributes
来精确的说明你的 API 支持什么。REST API 的客户端期望知道映射到具体逻辑操作上的路径和 HTTP 谓词。
由于一个特性路由应用于一个特定操作,很容易使参数作为路由模板定义中必须的一部分。在这个例子中,id
是必须作为 URL 路径中一部分的。
public class ProductsApiController : Controller
{
[HttpGet("/products/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id) { ... }
}
ProductsApi.GetProducts(int)
操作会被 URL 路径 /products/3
执行,但不会被 URL 路径 /products
执行。查看 路由 获取路由模板以及相关选项的完整描述。
这个路由特性同时也定义了一个 Products_List
的 路由名称。路由名称可以用来生成基于特定路由的 URL。路由名称对路由的 URL 匹配行为没有影响,只用于 URL 的生成。路由名称必须在应用程序内唯一。
注解
常规的 默认路由 定义id
参数作为可选项 ({id?}
)。而特性路由的这种精确指定 API 的能力更有优势,比如把/products
和/products/5
分配到不同的操作。
联合路由
为了减少特性路由的重复部分, 控制器上的路由特性会和各个操作上的路由特性进行结合。任何定义在控制器上的路由模板都会作为操作路由模板的前缀。在控制器上放置一个路由特性会使 所有 这个控制器中的操作使用这个特性路由。
[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)
。两个操作都只会匹配 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("/")] // Does not 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();
}
}
特性路由的顺序
与常规路由的根据定义顺序来执行相比,特性路由构建一个树形结构同时匹配所有路由。这种行为看起来像路由条目被放置在一个理想的顺序中;最具体的路由会在一般的路由之前执行。
比如,路由 blog/search/{topic}
比 blog/{*article}
更加具体。从逻辑上讲,blog/search/{topic}
路由先‘运行’,因为在默认情况下这是唯一明智的排序。使用常规路由,开发者负责按所需的顺序放置路由。
特性路由可以配置顺序,通过使用所有提供路由特性的框架中的 Order
属性。路由根据 Order
属性升序处理。默认的 Order
是 0
。使用 Order = -1
设置一个路由,这个路由会在没有设置 Order
的路由之前运行。使用 Order = 1
会在默认路由排序之后运行。
小技巧
避免依赖于Order
。如果你的 URL 空间需要明确的顺序值来使路由正确,那么它可能使客户端混乱。一般的特性路由会通过 URL 匹配选择正确的路由。如果 URL 的生成的默认顺序不生效,使用路由名作为重载通常比应用Order
属性更简单。
路由模板中的标记替换([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() { ... }
[HttpPost("{id}")] // Matches '/api/Products/{id}'
public IActionResult Edit(int id) { ... }
}
标记替换也可以应用于在特性路由中定义路由名称。[Route("[controller]/[action]", Name="[controller]_[action]")]
将为每一个操作生成一个唯一的路由名称。
多路由
特性路由支持定义多个路由指向同一个操作。最常见的使用是像下面展示一样模仿 默认常规路由 :
[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 空间简单和定义明确。使用多个路由到操作上仅仅在需要的时候,比如支持已经存在的客户端。
使用 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; }
}
上面例子中,当 [MyApiController]
特性被应用,会自动设置 Template
为 "api/[controller]"
。
使用应用程序模型来自定义特性路由
应用程序模型 是一个在启动时创建的对象模型,它包含了所有 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;
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()
};
}
}
}
混合路由
MVC 应用程序可以混合使用常规路由和特性路由。对于给浏览器处理页面的控制器,通常使用常规路由;对于提供 REST API 的控制器,通常使用特性路由。
操作在常规路由或者特性路由中二选一。放置一个路由到控制器上或者操作上使操作变为特性路由。定义为特性路由的操作不能通过常规路由访问,反之亦然。放置在控制器上的 任何 路由特性都会使控制器中的所有操作变为特性路由。
注解
这两种路由系统的区别是通过 URL 匹配路由模板的过程。在常规路由中,匹配中的路由值被用来在所有常规路由操作的查找表中选择操作以及控制器。在特性路由中,每个模板已经关联了一个操作,进一步查找是没必要的。
URL 的生成
MVC 应用程序可以使用路由 URL 的生成特性来生成 URL 链接到操作。生成 URL 消除硬编码 URL,使你的代码健壮和易维护。这个章节关注 MVC 提供的 URL 生成特性,并只覆盖如何生成 URL 的基本知识。查看 路由 获取 URL 生成的详细描述。
IUrlHelper 接口是 MVC 与生成 URL 的路由之间基础设施的基本块。你可以通过控制器、视图以及视图组件中的 Url
属性找到一个可用的 IUrlHelper
实例。
在这个例子中,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 的生成工作与特性路由类似,尽管概念是不同的。在路由值常规路由中,路由值被用来扩大一个模板,并且关于 controller
和 action
的路由值通常出现在那个模板中 —— 这生效了,因为路由匹配的URL 坚持了一个 约定。在特性路由中,关于 controller
和 action
的路由值不被允许出现在模板中 —— 它们用来查找该使用哪个模板。
这个例子使用特性路由:
// 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 构建了一个所有特性路由操作的查找表并且会匹配 controller
和 action
值选择路由模板用于 URL 的生成。在上面的例子中,custom/url/to/destination
被生成了。
通过操作名生成 URL
Url.Action
( IUrlHelper 、 Action)以及所有相关的重载都是基于通过指定控制器名和操作名来指定想要链接到的地方的。
注解
当使用Url.Action
,controller
和action
的当前路由值是为你指定的 ——controller
和action
的值同时是 环境值 和 值 的一部分。Url.Action
方法总是使用controller
和action
的当前值并且生成路由到当前操作的 URL 路径。
路由尝试使用环境值中的值来填充信息,以至于在生成 URL 时你不需要提供信息。使用路由如 {a}/{b}/{c}/{d}
并且环境值 { a = Alice, b = Bob, c = Carol, d = David }
,路由拥有足够的信息生成路由而不需要任何额外的值 —— 因为所有的路由参数都有值。如果你添加值 { d = Donovan }
,那么值 { d = David }
会被忽略,并且生成的 URL 路径会是 Alice/Bob/Carol/Donovan
。
警告
URL 路径是分层次的。在上面的例子中,如果你添加值{ c = Cheryl }
,所有的值{ c = Carol, d = David }
会被忽略。在这种情况下,我们不再有d
的值,且 URL 生成会失败。你需要指定c
和d
所需的值。你可能期望用默认路由 ({controller}/{action}/{id?}
) 来解决这个问题 —— 但是你很少会在实践中遇到这个问题,Url.Action
总会明确地指定controller
和action
的值。
Url.Action
较长的重载也采取额外的 路由值 对象来提供除了 controller
和 action
意外的路由参数。你最长看到的是使用 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)
通过路由生成 URL
上面的代码展示了通过传递控制器名和操作名创建 URL。IUrlHelper
也提供 Url.RouteUrl
的系列方法。这些方法类似 Url.Action
,但是它们不复制 action
和 controller
的当前值到路由值。最常见的是指定一个路由名来使用具体的路由生成 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 中生成URL
IHtmlHelper 提供 HtmlHelper 方法 Html.BeginForm
和 Html.ActionLink
来分别生成 <form>
和 <a>
元素。这些方法使用 Url.Action
方法来生成一个 URL 并且它们接受类似的参数。Url.RouteUrl
相对于 HtmlHelper 的是 Html.BeginRouteForm
和 Html.RouteLink
,它们有着类似的功能。查看 :doc:/mvc/views/html-helpers
获取更多信息。
TagHelper 通过 form
和 <a>
TagHelper 生成 URL。这些 都使用了 IUrlHelper
为它们的实现。查看 Working with Forms 获取更多信息。
内部观点,IUrlHelper 通过 Url
属性生成任何不包含上述的特定 URL。
在操作结果中生成 URL
上面的例子展示了在控制器中使用 IUrlHelper
,而在控制器中最常见的用法是生成一个 URL 作为操作结果的一部分。
ControllerBase
和 Controller
基类针对引用其他操作的操作结果提供了方便的方法。一个典型的使用是接受用户输入后重定向。
public Task<IActionResult> Edit(int id, Customer customer)
{
if (ModelState.IsValid)
{
// Update DB with new details.
return RedirectToAction("Index");
}
}
操作结果工厂方法遵循 IUrlHelper 中类似模式的方法。
专用常规路由的特殊情况
常规路由可以使用一种特殊的路由被称作 专用常规路由。在下面的例子中,被命名为 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 路径 /
,但是为什么呢?你可能会猜路由值 { controller = Home, action = Index }
会足以用 blog
路由来生成 URL,并且结果会是 /blog?action=Index&controller=Home
。
专用常规路由依靠默认路由的一个特殊行为,没有相应的路由参数,以防止路由生成 URL “太贪婪”。在这种情况下默认的值是 { controller = Blog, action = Article }
,而不是出现在路由参数中的 controller
或者 action
。当路由执行 URL 的生成,提供的值必须匹配默认路由。URL 的生成使用 blog
将失败,因为值 { controller = Home, action = Index }
不匹配 { controller = Blog, action = Article }
。然后路由回退到尝试 default
,并成功。
区域
区域 是一个 MVC 特点,用来组织相关的功能到一个单独的路由命名空间(针对控制器操作)的组和单独的文件夹结构中(针对视图)。使用区域允许一个应用程序拥有多个同名的路由器 —— 只要它们有不同的 区域。使用区域达到通过添加另一个路由参数分层的目的,area
到 controller
以及 action
。这个章节将讨论如何路由作用于区域 —— 查看 区域 获取区域如何与视图配合使用的详细信息。
下面的例子使用默认常规路由配置 MVC,以及一个命名为 Blog
的 区域路由:
app.UseMvc(routes =>
{
routes.MapAreaRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});
当匹配 URL 路径如 /Manage/Users/AddUser
时,第一个路由会产生路由值 { area = Blog, controller = Users, action = AddUser }
。area
路由值是通过 area
的默认值产生的,实际上通过 MapAreaRoute
创建路由和下面的方式是相等的:
app.UseMvc(routes =>
{
routes.MapRoute("blog_route", "Manage/{controller}/{action}/{id?}",
defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});
MapAreaRoute 创建一个路由同时使用默认路由和 area
约束,约束使用提供的区域名,在这个例子中是 Blog
。默认值保证路由总是处理 { area = Blog, ... }
,约束要求值 { area = Blog, ... }
来进行 URL 的生成。
小技巧
常规路由是顺序依赖。一般来说,区域路由需要被放置在路由表的前面,因为没有比区域路由更具体的路由了。
使用上述例子,路由值将匹配下面操作:
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
AreaAttribute 表示控制器属于一个区域的一部分,我们说,这个控制器是在 Blog
区域。控制器不带 [Area]
特性则不是任何区域的成员,并且当 area
路由值通过路由提供时 不会 匹配。在下面的例子中,只有第一个列出的控制器可以匹配路由值 { area = Blog, controller = Users, action = AddUser }
。
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace2
{
// Matches { area = Zebra, controller = Users, action = AddUser }
[Area("Zebra")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
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
{
public IActionResult AddUser()
{
return View();
}
}
}
注解
为了完整性,将每个控制器的命名空间显示到这里 —— 否则控制器将会遇到命名冲突并且声称一个编译错误。类命名空间不影响 MVC 的路由。
前两个控制器是区域的成员,并只匹配通过 area
路由值提供的各自的区域名。第三个控制器不是任何区域的成员,只会在路由中没有 area
值时匹配。
注解
在匹配 no value 方面,缺少area
值与area
是 null 或者空字符串是一样的。
当执行一个区域内的操作时,area
的路由值可作为用于生成 URL 的 环境值。这意味着默认情况下区域针对 URL 的生成有 黏性 ,如下面例子所示。
app.UseMvc(routes =>
{
routes.MapAreaRoute("duck_route", "Duck",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default", "Manage/{controller=Home}/{action=Index}/{id?}");
});
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace4
{
[Area("Duck")]
public class UsersController : Controller
{
public IActionResult GenerateURLInArea()
{
// Uses the 'ambient' value of area
var url = Url.Action("Index", "Home");
// returns /Manage
return Content(url);
}
public IActionResult GenerateURLOutsideOfArea()
{
// Uses the empty value for area
var url = Url.Action("Index", "Home", new { area = "" });
// returns /Manage/Home/Index
return Content(url);
}
}
}
理解 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 谓词。所以假设有一个 POST
操作 —— 只有 Edit(...)
会匹配。但是如果是 GET
两个操作都会匹配 —— 然而,一个操作使用了 IActionConstraint
总是被认为 更好 与没有使用的操作。所以因为 Edit()
有 [HttpGet]
,它被视为更加具体,并且在两个操作都可以匹配时被选中。
从概念上讲, IActionConstraint
是 重载 的一种形式,但不是使用相同名称的重载方法,它是匹配相同 URL 的操作的重载。特性路由也使用 IActionConstraint
并且可能导致不同控制器的操作被视为候选。
实现 IActionConstraint
实现 IActionConstraint 最简单的方式是创建一个类派生自 System.Attribute
并且将它放置到你的操作和控制器上。MVC 会自动发现任何作为特性被应用的 IActionConstraint
。你可以使用应用程序模型来应用约束,并且这可能是最灵活的方法,因为它可以允许你对它们如何被应用进行元编程。
在下面的例子,一个约束选择一个操作基于一个来自路由数据的 country code 。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
方法并选择一个 ‘Order’ 用于约束执行。在这个例子中,Accept
方法返回 true
表示当 country
路由值匹配时操作是匹配的。这和 RouteValueAttribute
不同,因为它允许回退到一个非特性操作。这个例子展示了如果你定义一个 en-US
操作,然后国家代码是 fr-FR
会回退到一个更通用的控制器,这个控制器没有应用 [CountrySpecific(...)]
。
Order
特性决定约束的部分是哪个阶段。操作约束基于 Order
在组中运行。比如,所有框架提供的 HTTP 方法特性使用相同 Order
值,所以他们运行在同一阶段。你可以拥有许多阶段,来实现你所需要的策略。
小技巧
要决定一个Order
的值,考虑你的约束是否需要在 HTTP 方法之前被应用。数字越低,运行越早。