ASP.NET Core MVC学习教程之路由(Routing)
前言
ASP.NETCoreMVC路由是建立在ASP.NETCore路由的,一项强大的URL映射组件,它可以构建具有理解和搜索网址的应用程序。这使得我们可以自定义应用程序的URL命名形式,使得它在搜索引擎优化(SEO)和链接生成中运行良好,而不用关心Web服务器上的文件是怎么组织的。我们可以方便的使用路由模板语法定义路由,路由模板语法支持路由值约束,默认值和可选值。
基于约束的路由允许全局定义应用支持的URL格式,以及这些格式是怎样各自在给定的控制器中映射到指定的操作方法(Action)。当接受到一个请求时,路由引擎解析URL并将其匹配至一个定义URL格式,然后调用相关的控制器操作方法。
routes.MapRoute( name:"default", template:"{controller=Home}/{action=Index}/{id?}");
特性路由(AttributeRouting)允许以在控制器和方法使用添加特性的方式指定路由信息来定义应用程序的路由。这意味着路由定义紧邻它们所关联的控制器和方法。
ASP.NETCoreMVC使用路由中间件来匹配传入请求的URL,并将它们映射到操作方法。路由在启动代码或属性中定义,它描述了网址路径应如何与操作方法匹配,还用于响应中生成链接并发送。
1.设置路由中间件
创建一个ASP.NETCoreWeb应用程序,在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类似:
varroute=newRouteBuilder(app); //添加连接到MVC,通过调用MapRoute来回调 route.DefaultHandler=newMvcRouteHandler(...); //执行回调以注册路由 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必须消除歧义,选择最佳操作,否则会抛出异常。例如:
publicclassBlogController:Controller { publicActionResultEdit(intid) { returnView(); } [HttpPost] publicActionResultEdit(intid,IFormCollectioncollection) { try { //TODO:Addupdatelogichere returnRedirectToAction(nameof(Index)); } catch { returnView(); } } }
URL/Blog/Edit/7可以匹配这两个操作,这是MVC控制器的典型模式,其中Edit(int)用于显示编辑的表单,Edit(int,IFormCollection)用于处理已提交的表单。为了达到这个目的,MVC需要在HTTPPOST时选择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();没有传递路由。
publicclassHomeController:Controller { [Route("")] [Route("Home")] [Route("Home/Index")] publicIActionResultIndex() { returnView(); } [Route("Home/About")] publicIActionResultAbout() { ViewData["Message"]="Yourapplicationdescriptionpage."; returnView(); } }
HomeController.Index操作将会对/,/Home或者/Home/Index任一URL访问执行。
特性路由需要有更多的输入来指定一个路由,而常规路由处理路由时更加简洁。然而,特性路由允许精准控制每个操作的路由模板。
上面的模板中没有定义针对action,area,controller的路由参数。实际上,这些参数不允许出现在特性路由中,因为路由模板已经关联了一个操作,解析URL中的操作名是没有意义的。
特性路由也可以使用HTTP[Verb]特性,如HTTPPost:
[HttpGet("/Blog")] publicActionResultIndex() { returnView(); }
由于特性路由适用于特定操作,因此很容易使参数作为模板定义中必须的一部分。下面的例子,id是URL中必须的一部分:
[HttpGet("Blog/Edit/{id}")] publicActionResultEdit(intid) { returnView(); }
常规的默认路由定义id参数作为可选项({id?}),而特性路由的是必须参数,这种可以精准指定,比如包/Blog/Get和/Blog/Get/{id}分配到不同的操作。
5.组合路由
为了减少特性路由的重复部分,控制器上的路由特性会和各个操作上的路由特性进行结合。任何定义在控制器上的路由模板都会作为操作路由模板的前缀。
[Route("blog")] publicclassBlogController:Controller { [HttpGet] publicActionResultGetAll() { returnView(); } [HttpGet("{id}")] publicActionResultGetById(intid) { returnView(); } }
/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]")] publicclassBlogController:Controller { [HttpGet]//匹配Blog/GetAll publicActionResultGetAll() { returnView(); } }
标记替换发生在构建特性路由的最后一步。与上面结果相同的写法:
publicclassBlogController:Controller { [HttpGet("[controller]/[action]")]//匹配Blog/GetAll publicActionResultGetAll() { returnView(); } }
特性路由也可以与继承相结合,即继承父类的路由标记。
特性路由支持单个操作定义路由。如果用IActionConstarint实现的多个路由特性定义在一个操作上时,每个操作约束与特性定义的路由相结合:
[Route("Store")] [Route("[controller]")] publicclassBlogController:Controller { [HttpGet("GetAll")]//匹配GetBlog/GetAll和Store/GetAll [HttpPost("Set")]//匹配PostBlog/Set和Store/Set publicActionResultGetAll() { returnView(); } }
虽然使用多个路由到一个操作看起来很强大,但最好还是保持URL的空间简单和定义明确。使用多个路由到操作上仅仅在特殊需要的时候,比如支持多个客户端。
8.使用IRouteTemplateProvider自定义路由特性
所有框架提供的路由特性([Route(...)],[HttpGet(...)]等)都实现了IRouteTemplateProvider接口。当程序启动时,MVC查找控制器类和操作方法上都实现IRouteTemplateProvider接口的特性来构建储时路由集合。
可以通过实现IRouteTemplateProvider来定义自己的路由特性。每个IRouteTemplateProvider都允许定义使用自定义路由模板,顺序以及名称的单一路由:
publicclassMyApiControllerAttribute:Attribute,IRouteTemplateProvider { publicstringTemplate=>"api/[controller]"; publicint?Order{get;set;} publicstringName{get;set;} }
当[MyApiController]特性被应用时,会自动设置Template为api/[controller]。
9.使用应用程序模型来自定义特性路由
应用程序模型时启动时创建的对象模型,其中包含MVC用于路由和执行操作的所有元数据。应用程序模型包括从路由特性收集的所有数据(通过IRouteTemplateProvider)。我们可以编写约定以在启动时修改应用程序模型为自定义路由行为。
publicclassNamespaceRoutingConvention:IControllerModelConvention { privatereadonlystring_baseNamespace; publicNamespaceRoutingConvention(stringbaseNamespace) { _baseNamespace=baseNamespace; } publicvoidApply(ControllerModelcontroller) { varhasRouteAttributes=controller.Selectors.Any(selector=> selector.AttributeRouteModel!=null); if(hasRouteAttributes) { //此控制器自定义了一些路由,因此将其视为覆盖 return; } //使用命名空间和控制器来推断控制器的路由 // //Example: // //controller.ControllerTypeInfo->"My.Application.Admin.UsersController" //baseNamespace->"My.Application" // //template=>"Admin/[controller]" // //这使得你的路由大致与你的项目结构一致 // varnamespc=controller.ControllerType.Namespace; vartemplate=newStringBuilder(); template.Append(namespc,_baseNamespace.Length+1,namespc.Length-_baseNamespace.Length-1); template.Replace('.','/'); template.Append("/[controller]"); foreach(varselectorincontroller.Selectors) { selector.AttributeRouteModel=newAttributeRouteModel() { Template=template.ToString() }; } } }
这部分怎么使用,个人还是不是很清楚,这里只是记录了官方文档,有哪位知道可以告诉以下小弟。
10.URL生成
MVC应用程序可以使用路由URL的生成特性来生成URL链接到操作。生成URL可以消除硬编码URL,使代码更加健壮和易维护。IUrlHelper接口是MVC与生成URL路由之间基础设施的基本块。可以通过控制器,视图以及视图组件中的URL属性找到一个可用的IUrlHelper实例:
publicclassHomeController:Controller { publicIActionResultIndex() { //生成/Home/Contact varurl=Url.Action("Contact"); returnView(); } publicIActionResultContact() { ViewData["Message"]="Yourapplicationdescriptionpage."; returnView(); } }
这个URL路径是由路由值与当前请求相结合而成的路由创建,并将值传递给Url.Action,替换路由模板中对应的值。
上面Url.Action(的例子是常规路由,但是URL的生成工作与特性路由类似,尽管概念不同。在常规路由中,路由值被用来扩展模板,并且关于controller和action的路由值通常出现在那个模板,因为路由匹配的URL坚持了一个约定。在特性路由中,关于controller和action的路由值不允许出现在模板中--它们用来查找应该使用哪个模板,例如:
//修改Configure publicvoidConfigure(IApplicationBuilderapp,IHostingEnvironmentenv) { app.UseMvc(); } publicclassHomeController:Controller { [HttpGet("/")] publicIActionResultIndex() { //生成/Home/To/About varurl=Url.Action("About"); returnView(); } [HttpGet("Home/To/About")] publicIActionResultAbout() { ViewData["Message"]="Yourapplicationdescriptionpage."; returnView(); } }
MVC构建了一个所有特性路由操作的查找表,并且会匹配controller和action值来选择路由模板用于生成URL。
11.通过操作名生成URL
Url.Action(thisIUrlHelperhelper,stringaction)以及所有相关的重载都是基于指定控制器名称和操作名来指定要链接的内容。
当使用Url.Action时,controller和action的当前路由值是指定的--controller和action的值同时是环境值和值的一部分。Url.Action方法总是使用controller和action的当前值,并且生成路由到当前操作的URL路径。
路由尝试使用环境值中的值来填充信息,同时我们也可以指定路由参数:
publicclassHomeController:Controller { publicIActionResultIndex() { //生成/Blog/Edit/1 varurl=Url.Action("Edit","Blog",new{id=1}); //生成/Blog/Edit/1?color=red varurl1=Url.Action("Edit","Blog",new{id=1,color="red"}); returnView(); } }
如果像创建一个绝对URL,可以使用一个接受protocol的重载:Url.Action("Edit","Blog",new{id=1},protocol:Request.Scheme);
12.通过路由名生成URL
IUrlHelper也提供了Url.RouteUrl的系列方法,最常见的是指定一个路由名来使用具体的路由生成URL,通常没有指定控制器名或操作名:
publicclassHomeController:Controller { publicIActionResultIndex() { //生成customer/to/url varurl=Url.RouteUrl("AboutRoute"); returnView(); } [HttpGet("customer/to/url",Name="AboutRoute")] publicIActionResultAbout() { ViewData["Message"]="Yourapplicationdescriptionpage."; returnView(); } }
在HTML中生成的URLHtmlHelper,提供了HtmlHelper方法Html.BeginForm和Html.ActionLink来分别生成