ASP.NET Core MVC 之路由(Routing)
ASP.NET Core MVC 路由是建立在ASP.NET Core 路由的,一项强大的URL映射组件,它可以构建具有理解和搜索网址的应用程序。这使得我们可以自定义应用程序的URL命名形式,使得它在搜索引擎优化(SEO)和链接生成中运行良好,而不用关心Web服务器上的文件是怎么组织的。我们可以方便的使用路由模板语法定义路由,路由模板语法支持路由值约束,默认值和可选值。
基于约束的路由允许全局定义应用支持的URL格式,以及这些格式是怎样各自在给定的控制器中映射到指定的操作方法(Action)。当接受到一个请求时,路由引擎解析URL并将其匹配至一个定义URL格式,然后调用相关的控制器操作方法。
routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}");
特性路由(Attribute Routing) 允许以在控制器和方法使用添加特性的方式指定路由信息来定义应用程序的路由。这意味着路由定义紧邻它们所关联的控制器和方法。
ASP.NET Core MVC 使用路由中间件来匹配传入请求的URL,并将它们映射到操作方法。路由在启动代码或属性中定义,它描述了网址路径应如何与操作方法匹配,还用于响应中生成链接并发送。
1.设置路由中间件
创建一个ASP.NET Core Web应用程序,在Startup类的Configure方法中有:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
在对UseMvc的调用过程中,MapRoute用于创建单个路由,即默认路由。大多数MVC应用程序都使用与默认路由模板类似的路由。
路由模板{controller=Home}/{action=Index}/{id?} 可以匹配类似 Blog/Details/5 的URL路径,并且提取路由值 {controller=Blog,action=Details,id=5}。MVC将尝试查找名为BlogController的控制器,并运行操作方法。
{controller=Home}将Home定义为默认控制器
{action=Index}将Index定义为默认操作
{id?}将id定义为可选
默认路径参数和可选路径参数可以不出现在需要匹配的URL路径中。
使用{controller=Home}/{action=Index}/{id?}模板,可以对以下URL路径都执行HomeController.Index:
/Home/Index/7
/Home/Index
/Home
/
有个简单方法 app.UseMvcWithDefaultRoute() 可以替换掉上面的方法。
UseMvc 和 UseMvcWithDefaultRoute 都是将RouteMiddleware的实例添加到中间件管道。MVC不直接与中间件交互,而是使用路由来处理请求。MVC通过MvcRouteHandler的实例链接到路由。下面的代码与UseMvc类似:
var route = new RouteBuilder(app); //添加连接到MVC,通过调用MapRoute来回调 route.DefaultHandler = new MvcRouteHandler(...); //执行回调以注册路由 route.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); //创建路由集合并添加至中间件 app.UseRouter(route.Build());
UseMvc 不直接定义任何路由,它为属性路由的路由集合添加一个占位符。重载UseMvc 使得我们可以添加自己的路由,并且还支持属性路由。UseMvc 及其所有变体为属性路由添加了占位符,这使得属性路由始终可用。UseMvcWithDefaultRoute定义默认路由并且支持属性路由。
2.常规路由
routes.MapRoute(name: "default",template: "{controller=Home}/{action=Index}/{id?}"); 这是一个常规路由,因为它建立了一个约定的URL路径:
第一路径段映射到控制器名称
第二路径映射到操作名称
第三区段是可选id,用于映射到模型实体
使用default路由,URL 路径 /Blog/Index 将映射到BlogController.Index 操作。该映射是基于控制器和操作名称,而不是基于命名空间,源文件位置等。
使用常规路由的默认路由可以快速构建应用程序,而无需定义每一个操作的路由。对于CRUD 操作风格的应用程序,整个控制器的URL具有一致性。
3.多路由
可以在UseMvc 里面通过添加MapRoute 来添加多个路由。这样可以定义多个约定,或添加专用于特定操作的常规路由:
app.UseMvc(routes => { routes.MapRoute("blog", "blog/{*article}", defaults: new { Controller = "Blog", Action = "Index" }); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
这里的blog 路由是专用常规路由,这意味着它不采用常规路由系统,而是专用于一个特定地的动作。这个路由始终映射到BlogController.Index。
路由集合中的路由是有序的,并且会按照它们被添加的顺序进行处理。
1.回退
作为请求处理的一部分,MVC将验证路由值是否可以用来查找应用程序中的控制器和操作。如果路由值不匹配操作,那么该路由被认为是不匹配的,将尝试下一个路由。这一过程称为回退,因为常规路由有重叠的情况。
2.行动歧义
当两个一致的操作通过路由时,MVC必须消除歧义,选择最佳操作,否则会抛出异常。例如:
public class BlogController : Controller { public ActionResult Edit(int id) { return View(); } [HttpPost] public ActionResult Edit(int id, IFormCollection collection) { try { // TODO: Add update logic here return RedirectToAction(nameof(Index)); } catch { return View(); } } }
URL /Blog/Edit/7 可以匹配这两个操作,这是MVC控制器的典型模式,其中Edit(int)用于显示编辑的表单,Edit(int,IFormCollection)用于 处理已提交的表单。为了达到这个目的,MVC需要在HTTP POST时选择Edit(int,IFormCollection),在其他HTTP动词时选择Edit(int)。
HttpPostAttribute 是IActionConstraint 的一个实现,它只允许在HTTP动词为POST时选择动作。IActionConstraint的存在使得Edit(int,IFormCollection)比Edit(int)更好匹配。
如果有多个路由匹配,并且MVC无法找到一个最佳路由,则会抛出AmbiguousActionException异常。
3.路由名称
上面的例子中"blog"和"default"字符串是路由名称,路由名称为路由提供了一个逻辑名称,以便命名的路由可用于生成URL。在应用程序范围内路由必须名称必须是唯一的。
路由名称对URL匹配或请求的处理没有影响,仅用于URL生成。
4.路由特性
特性路由使用一组特性直接将操作映射到路由模板。下面在Configure中调用 app.UseMvc(); 没有传递路由。
public class HomeController : Controller { [Route("")] [Route("Home")] [Route("Home/Index")] public IActionResult Index() { return View(); } [Route("Home/About")] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); } }
HomeController.Index操作将会对 /,/Home 或者/Home/Index 任一URL访问执行。
特性路由需要有更多的输入来指定一个路由,而常规路由处理路由时更加简洁。然而,特性路由允许精准控制每个操作的路由模板。
上面的模板中没有定义针对 action,area ,controller的路由参数。实际上,这些参数不允许出现在特性路由中,因为路由模板已经关联了一个操作,解析URL中的操作名是没有意义的。
特性路由也可以使用HTTP[Verb]特性,如HTTPPost:
[HttpGet("/Blog")] public ActionResult Index() { return View(); }
由于特性路由适用于特定操作,因此很容易使参数作为模板定义中必须的一部分。下面的例子,id是URL中必须的一部分:
[HttpGet("Blog/Edit/{id}")] public ActionResult Edit(int id) { return View(); }
常规的默认路由定义id参数作为可选项({id?}),而特性路由的是必须参数,这种可以精准指定,比如包/Blog/Get 和 /Blog/Get/{id} 分配到不同的操作。
5.组合路由
为了减少特性路由的重复部分,控制器上的路由特性会和各个操作上的路由特性进行结合。任何定义在控制器上的路由模板都会作为操作路由模板的前缀。
[Route("blog")] public class BlogController : Controller { [HttpGet] public ActionResult GetAll() { return View(); } [HttpGet("{id}")] public ActionResult GetById(int id) { return View(); } }
/blog 匹配GetAll方法, /blog/1 匹配 GetById方法。
注意,如果操作上路由模板以 / 开头时不会结合控制器上的路由模板。
6.特性路由的顺序
常规路由会根据定义顺序来执行,与之相比,特性路由会构建一个树形结构,同时匹配所有路由。这种看起来像路由条目被放置在一个理想的顺序中,最具体的路由会在一般的路由之前执行。比如,路由blog/Edit/4 比 blog/{*article} 更加具体。
特性路由使用所有框架提供的路由特有的Order属性来配置顺序,并根据Order属性升序处理路由。默认是0,设置为-1时会在没有设置的路由之前执行。
7.路由模板中的标记替换( [controller] , [action] , [area])
为了方便,特性路由支持标记替换,即通过在在方括号中封闭一个标记([, ])来替换对应的名称。标记[action],[area],[controller]会被替换成操作所对应的操作名,区域名,控制器名。
[Route("[controller]/[action]")] public class BlogController : Controller { [HttpGet]//匹配Blog/GetAll public ActionResult GetAll() { return View(); } }
标记替换发生在构建特性路由的最后一步。与上面结果相同的写法:
public class BlogController : Controller { [HttpGet("[controller]/[action]")]//匹配Blog/GetAll public ActionResult GetAll() { return View(); } }
特性路由也可以与继承相结合,即继承父类的路由标记。
特性路由支持单个操作定义路由。如果用IActionConstarint实现的多个路由特性定义在一个操作上时,每个操作约束与特性定义的路由相结合:
[Route("Store")] [Route("[controller]")] public class BlogController : Controller { [HttpGet("GetAll")]//匹配 Get Blog/GetAll和 Store/GetAll [HttpPost("Set")]//匹配 Post Blog/Set和 Store/Set public ActionResult GetAll() { return View(); } }
虽然使用多个路由到一个操作看起来很强大,但最好还是保持URL的空间简单和定义明确。使用多个路由到操作上仅仅在特殊需要的时候,比如支持多个客户端。
8.使用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] 。
9.使用应用程序模型来自定义特性路由
应用程序模型时启动时创建的对象模型,其中包含MVC用于路由和执行操作的所有元数据。应用程序模型包括从路由特性收集的所有数据(通过 IRouteTemplateProvider)。我们可以编写约定以在启动时修改应用程序模型为自定义路由行为。
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) { //此控制器自定义了一些路由,因此将其视为覆盖 return; } // 使用命名空间和控制器来推断控制器的路由 // // Example: // // controller.ControllerTypeInfo -> "My.Application.Admin.UsersController" // baseNamespace -> "My.Application" // // template => "Admin/[controller]" // // 这使得你的路由大致与你的项目结构一致 // 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() }; } } }
这部分怎么使用,个人还是不是很清楚,这里只是记录了官方文档,有哪位知道可以告诉以下小弟。
10.URL生成
MVC应用程序可以使用路由URL的生成特性来生成URL链接到操作。生成URL可以消除硬编码URL,使代码更加健壮和易维护。IUrlHelper 接口是MVC与生成URL路由之间基础设施的基本块。可以通过控制器,视图以及视图组件中的URL属性找到一个可用的IUrlHelper实例:
public class HomeController : Controller { public IActionResult Index() { //生成/Home/Contact var url = Url.Action("Contact"); return View(); } public IActionResult Contact() { ViewData["Message"] = "Your application description page."; return View(); } }
这个URL路径是由路由值与当前请求相结合而成的路由创建,并将值传递给Url.Action,替换路由模板中对应的值。
上面Url.Action(的例子是常规路由,但是URL的生成工作与特性路由类似,尽管概念不同。在常规路由中,路由值被用来扩展模板,并且关于controller和action的路由值通常出现在那个模板,因为路由匹配的URL坚持了一个约定。在特性路由中,关于controller和action的路由值不允许出现在模板中--它们用来查找应该使用哪个模板,例如:
//修改Configure public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc(); } public class HomeController : Controller { [HttpGet("/")] public IActionResult Index() { //生成/Home/To/About var url = Url.Action("About"); return View(); } [HttpGet("Home/To/About")] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); } }
MVC构建了一个所有特性路由操作的查找表,并且会匹配controller和action值来选择路由模板用于生成URL。
11.通过操作名生成URL
Url.Action(this IUrlHelper helper, string action) 以及所有相关的重载都是基于指定控制器名称和操作名来指定要链接的内容。
当使用Url.Action时,controller 和 action 的当前路由值是指定的 -- controller 和 action 的值同时是环境值和值的一部分。Url.Action 方法总是使用 controller 和 action 的当前值,并且生成路由到当前操作的URL路径。
路由尝试使用环境值中的值来填充信息,同时我们也可以指定路由参数:
public class HomeController : Controller { public IActionResult Index() { //生成/Blog/Edit/1 var url = Url.Action("Edit", "Blog",new { id=1}); //生成/Blog/Edit/1?color=red var url1 = Url.Action("Edit", "Blog", new { id = 1 ,color="red"}); return View(); } }
如果像创建一个绝对URL,可以使用一个接受protocol的重载: Url.Action("Edit", "Blog",new { id=1},protocol:Request.Scheme);
12.通过路由名生成URL
IUrlHelper也提供了 Url.RouteUrl 的系列方法,最常见的是指定一个路由名来使用具体的路由生成URL,通常没有指定控制器名或操作名:
public class HomeController : Controller { public IActionResult Index() { //生成customer/to/url var url = Url.RouteUrl("AboutRoute"); return View(); } [HttpGet("customer/to/url",Name = "AboutRoute")] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); } }
在HTML 中生成的URLHtmlHelper,提供了 HtmlHelper 方法 Html.BeginForm 和 Html.ActionLink 来分别生成<form>和<a>元素。这些方法使用Url.Action方法来生成一个URL,并且它们接受类似的参数。Url.RouteLink ,它们有类似的功能。TagHelper通过form和<a> TagHelper生成URL。这些都使用了IUrlHelper 作为它们的实现。在内部视图中,IUrlHelper 通过Url 属性生成任何不包含上述的特定URL。
13.在操作结果中生成URL
在控制器中常见的一个用法是生成一个URL作为操作结果的一部分。Controller和ControllerBase 基类为引用其他操作的操作结果提供了简单方法。一个典型的方法:
public class HomeController : Controller { public IActionResult Index() { //生成customer/to/url var url = Url.RouteUrl("AboutRoute"); return Redirect(url); //或者 //return RedirectToAction("Contact"); } [HttpGet("customer/to/url",Name = "AboutRoute")] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); } public IActionResult Contact() { ViewData["Message"] = "Your contact page."; return View(); } }
RedirectToAction方法有多个重载可以使用。
14.专用常规路由的特殊情况
有一种特殊的路由被称为专用常规路由,下面被命名为blog的路由就是:
app.UseMvc(routes => { routes.MapRoute("blog", "blog/{*article}", defaults: new { Controller = "Blog", Action = "Index" }); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
Url.Action("Index", "Home") 会使用默认路由生成URL。
专用常规路由是依靠默认路由的一个特殊行为,没有相应的路由参数,以防止路由生成URL“太贪婪”。当路由执行URL生成时,提供的值必须与默认值匹配:否则使用blog的URL生成失败,因为值 {controller=Home,action=Index}不匹配{controller=Blog,action=Index}。然后路由回退尝试default,并成功。
15.区域
Areas 是一种MVC功能,用来将相关功能组织为一个组,作为单独的路由命名空间(用于控制器操作)和文件夹结构(用于视图)。使用区域允许应用程序拥有多个相同名称的控制器——只要它们具有不同的区域。使用区域通过向控制器和操作添加另一个路由参数,区域可创建用于路由目的的层次结构。
使用默认常规路由配置MVC,命名一个OMS区域的路由:
app.UseMvc(routes => { routes.MapAreaRoute("oms", "OMS", "OManage/{controller}/{action}/{id?}", defaults: new { Controller = "Order", Action = "Index" }); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
namespace Blog.Areas.OMS.Controllers { [Area("OMS")] public class OrderController : Controller { // GET: Order public ActionResult Index() { return View(); } }
当URL为 /OManage/Order/Edit 时,会匹配路由值 {area = OMS,controller = Order , action = Edit}。area路由值是通过area默认值产生的。使用MapRoute方法也可以实现:
routes.MapRoute("oms", "OManage/{controller}/{action}/{id?}", defaults:new {area="OMS" },constraints:new { area = "OMS" });
MapAreaRoute创建一个路由,同时使用默认路由和area 约束,约束使用提供的区域名 OMS。默认值保证路由总是处理 {area = OMS},约束要求值{area = OMS} 来进行URL生成。
常规路由是顺序依赖。一般区域路由放置在前面,因为区域路由更具体。
AreaAttribute表示控制器属于一个区域的一部分,即这个控制器是在 OMS 区域。控制器不带[Area] 特性则不属于任何区域。
当在区域内执行操作时,区域的路由值将作为环境值以用于URL生成,这意味着,在默认情况下,区域对URL生成具有黏性:
namespace Blog.Areas.OMS.Controllers { [Area("OMS")] public class OrderController : Controller { // GET: Order public ActionResult Index() { //生成/OManage/Home/Create var url = Url.Action("Create","Home"); //生成/Home/Create var url1 = Url.Action("Create", "Home",new { area=""}); return View(); } } }
16.IActionConstraint
通常应用程序不需要自定义 IActionConstraint,[HttpGet] 特性以及类似的特性实现 IActionConstraint 接口,以限制方法的执行。
当两个操作一模一样,其中一个操作使用 IActionConstraint,总是认为比没有使用的操作更好,因为它被视为更加具体,并且两个操作都可以在匹配是被选中。(没有使用的操作会匹配任何HTTP谓词)
概念上,IActionConstraint 是重载的一种形式,但不是使用相同名称的重载,它是匹配相同URL操作的重载。特性路由也使用 IActionConstraint ,并且可以导致不同控制器的操作都被视为候选操作。
实现 IActionConstraint 最简单的方式是创建一个类派生自 System.Attribute ,并且将它放置到操作和控制器上。MVC会自动发现任何作为特性被应用的 IActionConstraint。你可以使用程序模型来应用约束,可能是最灵活的方法,因为它允许对它们如何被应用进行元编程。
一个例子,一个约束选择一个基于来自路由数据的 country code 操作:
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 方法返回true,表示当country路由值匹配时操作时匹配的。这个和 RouteValueAttribute 不同,因为它允许回退到一个非特性操作。这个例子展示了如果定义一个 en-US 操作,然后国家代码是 fr-FR,则会回退到一个更通用的控制器,这个控制器没有应用 [CountrySpecific(...)] 。
Order属性和 [HttpGet] 特性中的Order属性一样,用来决定运行顺序。
· 聊一聊 C#前台线程 如何阻塞程序退出
· 几种数据库优化技巧
· 聊一聊坑人的 C# MySql.Data SDK
· 使用 .NET Core 实现一个自定义日志记录器
· [杂谈]如何选择:Session 还是 JWT?
· 一个.NET开源、易于使用的屏幕录制工具
· 【经验】几种数据库优化技巧
· C#中 Task 结合 CancellationTokenSource的妙用
· Superpower:一个基于 C# 的文本解析工具开源项目
· 反微服务架构(A Macro Services Framework)