ASP.NET Core 的 controllers 使用Routing 中间件匹配客户端的 url 请求,然后映射到对应的 controller 的处理方法(Action)上。
Actions 可以是 常规路由 或 属性路由 的映射。
MVC App一般使用常规路由。
REST APIs 应该使用属性路由。
使用 MapControllerRoute() 方法,创建一个单一路由。
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
这个单一路由的名字是"default"。
这个单一路由的模板是"{controller=Home}/{action=Index}/{id?}",Home是默认Controller名称,Index是默认Action名称。
这个路由模板:
如果有两个Controller 的定义是这样的:
public class ProductsController : Controller
{
public IActionResult Details(int id)
{
// use id;
}
}
public class HomeController : Controller
{
public IActionResult Index() { ... }
}
如果有 url是:/Products/Details/5,
对应路由模板的 “{controller=Home}/{action=Index}/{id?}”,
因为可以找到Products Controller,所以匹配了ProductsController
而以下 url:都会匹配到HomeController:
注意:匹配的过程只基于Controllers名称和Action名称,与代码的命名空间,文件夹层级,Action的参数无关。
可以多次调用 app.MapControllerRoute()。
app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
blog路由在这里是一个专用的常规路由,符合规则的 url 都会映射到BlogController的Article方法。
“/Blog”, “/Blog/Article”, 和 “/Blog/{any-string}” 都会映射到BlogController的Article方法。
blog路由优先于default路由进行匹配,因为它先被注册。
如果多个Action都被路由系统匹配,那么会:
比如url /Products/Edit/17 会同时匹配下面的两个 Action
public class ProductsController : Controller
{
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpPost]
public IActionResult Edit(int id, Product product)
{
return ControllerContext.MyDisplayRouteInfo(id, product.name);
}
}
而一般来说,只有id入参的Action,url 一般是GET方法,而入参是int id, Product product的Action,url一般是POST方法。
所以需要使用[HttpGet] 或者 [HttpPOST] 来标记Action,让路由系统匹配最佳的Action,HTTP Method属性包括:
属性路由没有像传统路由那样的通配路由,而且不再通过Controller和Action的名称匹配,除非使用占位符匹配。
直接在Controller的Action中用属性定义:
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
[HttpGet]
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);
}
}
意思是一个Action可以匹配多个url模板。
但应该尽量精确的使用url模板,保证效率。
占位符可以是 [action], [area], 和 [controller],占位符使用area,controller和action的名称进行匹配。
例如:
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]")]
public class HomeController : Controller
{
[Route("~/")]
[Route("/Home")]
[Route("~/Home/Index")]
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
public IActionResult About()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
[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);
}
}
在Controller上定义了[Route(“products”)],在Action 上定义了HttpMethod,那么在匹配时,会组合起来匹配,比如:
但是 “/” 或者 “~/” 开头的模板不会被controller的模板组合。
可以在Url模板中指定参数id的值类型:
public class Products14Controller : Controller
{
[HttpPost("product14/{id:int}")]
public IActionResult ShowProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
还可以定义默认值:
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();
}
}
Actions 可以是传统路由或者属性路由,但是定义了属性路由的Actions 就不能再被传统路由匹配了,意思是属性路由的优先级更高。
而如果Controller上定义了属性路由,那么所有内部的Actions 就都不能再被传统路由匹配了。
保留的路由关键字不应该用在路由匹配系统中,包括:
ASP.NET Core App 可以生成指向Action的 URL,这个功能叫 URL Generation。
URL Generation 可以减少 hard code的URL,使代码更健壮,更好维护。
可以在controllers和views使用Url属性使用 IUrlHelper 接口的方法。
先看传统路由的例子:
public class UrlGenerationController : Controller
{
public IActionResult Source()
{
// 生成了url: /UrlGeneration/Destination
var url = Url.Action("Destination");
return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
}
public IActionResult Destination()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
这里使用的是传统路由,url 变量的值是URL的路径字符串 “/UrlGeneration/Destination”。通过组合http request的路由值(环境值):
路由模板中的每个路由参数会被环境值替代。没有值的路由参数要么使用默认值,要么跳过(可空)。
再看属性路由的例子:
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 action 生成了 custom/url/to/destination。
Url.Action, LinkGenerator.GetPathByAction 都用于通过Controller和Action的名称生成URL。
使用 Url.Action 时,controller 和 action的路由值由.NET 运行时生成。
假设有一个路由是这样: {a}/{b}/{c}/{d},环境值是 { a = Alice, b = Bob, c = Carol, d = David },那么路由有足够信息生成URL,不需要额外的值。
如果添加了{ d = Donovan },那么{ d = David }会被忽略,会生成URL Alice/Bob/Carol/Donovan。
如果添加了 { c = Cheryl },那么{ c = Carol, d = David } 都会被忽略。
任何不匹配路由参数的额外的值都会被放进query字符串中。比如:
public IActionResult Index()
{
var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
return Content(url!);
}
会生成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.RouteUrl 可以指定一个路由名称生成URL,而一般不指定Controller或者Action名称。
比如:
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();
}
下面的代码生成一个指向Destination_Route的链接:
<h1>Test Links</h1>
<ul>
<li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>
在Controlle中最常见的用法是在Action Results中返回一个URL,另一个用法是Redirect到另一个Action。
Controller基类提供了方便的方法来实现Redirect。
[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);
}
Area路由是MVC中的功能,可以把相关的功能做成一个分组。
可以通过Routing 命名空间分组controller 和 actions,也可以通过文件夹分组Views。
Areas 可以让多个controller 在不同的Area下有相同的名字。
比如:
app.MapAreaControllerRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
app.Run();
MapAreaControllerRoute创建的路由中,blog_route是路由名,Blog是Area名。
当匹配URL路径比如 /Manage/Users/AddUser 时,“blog_route” 路由生成的路由值是:{ area = Blog, controller = Users, action = AddUser }。
上面这个路由会匹配这个Action:
[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属性的Controller不会匹配任何带Area的路由模板。