原文:Routing
作者:Ryan Nowak、Steve Smith、 Rick Anderson
翻译:张仁建(Stoneqiu)
校对:许登洋(Seay)、谢炀(kiler398)、孟帅洋(书缘)、姚阿勇(Mr.Yao)
路由是用来把请求映射到路由处理程序。应用程序一启动就配置了路由,并且可以从URL中提取值用于处理请求。它还负责使用 ASP.NET 应用程序中定义的路由来生成链接。
这份文档涵盖了初级的ASP.NET核心路由。对于 ASP.NET 核心 MVC 路由, 请查看 Routing to Controller Actions
章节:
路由基础
路由使用 routes 类 ( IRouter 的实现) 做到:
- 映射传入的请求到 路由处理程序
- 生成响应中使用的 URLs
一般来说,一个应用会有单个路由集合。这个集合会按顺序处理。请求会在这个路由集合里按照 URL matching 来查找匹配。响应使用路由生成 URLs。
路由通过 RouterMiddleware 类连接到 middleware 管道。 作为配置的一部分 ASP.NET MVC 可以增加路由到中间件管道。 要了解如何使用路由作为独立组件, 请看 using-routing-middleware 。
URL 匹配
路由匹配指的是路由调度请求到一个处理程序的过程。这个过程通常是基于URL路径中的数据,但可以扩展到请求中的任何数据。调度请求到不同处理程序的能力是应用调节自身大小和复杂度的关键。
请求调用序列中每个路由的异步方法来进入路由中间件。IRouter 实例通过设置 RouteContext Handler 为一个不为空的 RequestDelegate 来选择是否处理请求。如果一个处理程序已经设置了路由,那么它就将被调用来处理这个请求,并且不会有其他的路由再去处理。如果所有的路由都执行了,请求还没有找到处理程序,那么中间件就会调用next方法,从而下一个在请求管道中的中间件就被调用了。
RouteAsync
的主要输入是和当前请求关联的 RouteContext HttpContext 。在一个成功匹配之后, RouteContext.Handler
和 RouteContext RouteData 会作为输出。
在 RouteAsync
执行期间,一个成功匹配会基于已经完成的请求处理设置 RouteContext.RouteData
的属性为合适的值。当一个路由成功匹配了一个请求时,RouteContext.RouteData
包含了重要的关于匹配结果的状态信息。
RouteData Values 是一个从路由产生的 路由值 字典。这些值通常由标记化的 URL 确定的,可以用来接收用户输入,或者用来在应用内部做更深层的调度决定。
RouteData DataTokens 是相关的匹配路由附加数据的属性包。提供 数据令牌
支持与每个路由相关的状态数据,这样应用基于匹配的路由可以迟点做出决定。这些数据是开发人员定义的,不会式影响路由的行为。而且,数据令牌中的值可以是任何类型,对比路由值,它可以很容易的转成字符串。
RouteData Routers 是一个成功匹配请求的路由列表。路由可以彼此嵌套,而且 Routers
属性反映了请求通过路由逻辑树导致匹配的路径。一般来说, Routers
中的第一项就是一个路由集合,而且应该用来生成URL。 Routers
中的最后一项就是已匹配路由。
URL 生成
URL 生成是指的路由基于一系列的路由值创建一个URL路径的过程。这允许你的处理程序和能访问它们的URL直接有一个逻辑分离。
路由生成遵循一个类似的迭代过程,但开始于用户或框架代码调用到路由集合的 GetVirtualPath 方法时。每个路由的 GetVirtualPath
方法都会被调用,直到返回一个不为空的 VirtualPathData。
GetVirtualPath
的主要输入是:
路由主要使用 Values
和 AmbientValues
提供的路由值来决定在哪儿生成一个 URL 以及包含什么值。 AmbientValues
是随着路由系统匹配当前请求而产生的一系列路由值。 相反,Values
是用于指定如何生成当前操作所需的URL的路由值。提供 HttpContext
是以防路由需要获取服务或当前上下文相关的数据。
建议
Values
看做是对AmbientValues
的重载。URL生成尝试重用来自当前请求的路由值,以便使用相同路由或路由值的链接更容易生成 URL。
GetVirtualPath
的输出是一个 VirtualPathData。VirtualPathData
是一个并行的 RouteData
;它包含了输出 URL 的虚拟路径以及应该由路由设置的一些额外的属性。
VirtualPathData VirtualPath 属性包含了路由生成的虚拟路径。根据你的需求,可能需要进一步处理的。例如,如果你想在 HTML 中呈现生成的 URL,你需要预先设置好应用的基础路径。
VirtualPathData Router 是一个成功生成URL路由的参考。
VirtualPathData DataTokens 属性是一个关联到生成URL的路由的附加数据的字典集合,这个和 RouteData.DataTokens
是并行的。
创建路由
路由提供了 Route 类作为 IRouter
的标准实现。当调用 RouteAsync 方法时, Route
使用 路由模板 语法定义匹配URL路径的模式。当调用 GetVirtualPath 方法时,Route
会使用相同的路由模板生成 URL。
大多数的应用会通过调用 MapRoute
方法或者定义在 IRouteBuilder 接口上的一个类似的扩展方法来创建路由。所有的这些方法会创建一个 Route
实例并添加到路由集合中。
注意
MapRoute 不需要路由处理参数--它只添加将被 DefaultHandler 处理的路由。由于默认处理程序是一个 IRouter 对象,可能决定不去处理请求。MVC通常配置了一个默认处理程序,它只处理能匹配到可用的控制器和操作的请求。了解更多关于MVC的路由,请点击 ? Routing to Controller Actions。
这是一个典型的ASP.NET MVC路由调用 MapRoute
的例子
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
这个模板会匹配像 /Products/Details/17
这样的URL路径,而且提取的路由值是 { controller = Products, action = Details, id = 17 }
。这个路由值由几个 URL 段组成,通过路由参数名称匹配路由模板中的每一段。路由参数被命名,它们是由大括号 { }
中的参数名定义的。
上面的这个模板也能匹配URL路径 /
,而且还将产生值为 { controller = Home, action = Index }
。这是因为 {controller}
和 {action}
都定义了默认值,而且 id
是可选的。路由参数名等号 =
后面的值是作为该参数的默认值。而参数名后面的问号 ?
标记着这个参数是可选的。带有默认参数的路由值 always 是会在路由匹配的时候产生一个路由值 - 而如果没有相应的 URL 路径段,可选参数将不会产生一个路由值。
查看路由模板特性和语法的完整描述请移步 route-template-reference。
这是一个 路由约束 的示例:
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id:int}");
这个模板将只会匹配像 /Products/Details/17
这样的URL路径,而不会匹配 /Products/Details/Apples
这样的。 {id:int}
为 id
这个路由参数定义了一个 路由约束 。路由约束实现自 IRouteConstraint
接口,它会检查并验证路由值。在这个例子中 id
必须可以转换为一个整形。更多详情请看 route-constraint-reference 。
另外,MapRoute
的重载接受 约束
, 数据令牌
, 和 默认值
。这些额外的参数都被定义为 object
类型。这些参数的典型用法是传递一个匿名类型的对象,其中匿名类型的属性名必须匹配路由参数的名称。
下面这两个例子创建的路由是等效的:
routes.MapRoute(
name: "default_route",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "default_route",
template: "{controller=Home}/{action=Index}/{id?}");
建议
对应简单的路由使用内嵌语法定义约束和默认值可能更方便些。然而,有些特性比如数据令牌嵌入式语法是不支持的。
这个例子展示了几个特点:
routes.MapRoute(
name: "blog",
template: "Blog/{*article}",
defaults: new { controller = "Blog", action = "ReadArticle" });
这个模板会匹配 /Blog/All-About-Routing/Introduction
这样的URL路径而且会提取出 { controller = Blog, action = ReadArticle, article = All-About-Routing/Introduction }
值。controller
和 action
的默认值是由路由产生,即使模板没有相应的路由参数。默认值可以在路由模板中指定。通过在路由参数名称前面增加一个 *
号, article
这个路由参数定义为一个 全部捕获 型 。 全部捕获型路由参数捕获剩下的URL路径,而且也能匹配空字符串。
增加路由约束和数据令牌的示例:
routes.MapRoute(
name: "us_english_products",
template: "en-US/Products/{id}",
defaults: new { controller = "Products", action = "Details" },
constraints: new { id = new IntRouteConstraint() },
dataTokens: new { locale = "en-US" });
这个模板会匹配像 /en-US/Products/5
这样的URL路径而且会提取出值为 { controller = Products, action = Details, id = 5 }
和数据令牌 { locale = en-US }
。
URL 生成
Route
类也可以通过结合一组路由值和路由模板来生成 URL。这在逻辑上是匹配URL路径的逆过程。
建议
为了更好的理解URL生成,想象一下你要生成的URL然后考虑一下一个路由模板该如何去匹配那个URL。会产生什么值?这和Route
类中 URL 是怎么样生成的是一个意思。
本例使用了一个基本的 ASP.NET MVC 风格的路由:
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
根据路由值 { controller = Products, action = List }
,这个路由会生成URL /Products/List
。路由值被相应的路由参数替换以形成URL路径。因为 id 是一个可选的路由参数,所以它没有值也没问题。
根据路由值 { controller = Home, action = Index }
,这个路由会生成 URL /
。因为提供的路由值匹配了默认值,因此可以安全地忽略相应的片段。注意到这两个URL生成在路由定义和产生用于生成 URL 的路由值之间是双向的。
建议
在ASP.NET MVC应用中应该使用 UrlHelper 类生成 URL,而不是直接调用路由。
更多关于 URL 生成过程的细节可以点击 url-generation-reference 。
使用路由中间件
在使用路由前,需要将其添加到 project.json 的 依赖性 中:
"Microsoft.AspNetCore.Routing": <current version>
在 Startup.cs 中添加路由到服务容器
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
路由必须在 Startup
类的 Configure
方法中配置。下面的例子使用了这些APIs:
- RouteBuilder
- Build
- MapGet 只匹配GET请求
- UseRouter
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
var trackPackageRouteHandler = new RouteHandler(context =>
{
var routeValues = context.GetRouteData().Values;
return context.Response.WriteAsync(
$"Hello! Route values: {string.Join(", ", routeValues)}");
});
var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);
routeBuilder.MapRoute(
"Track Package Route",
"package/{operation:regex(^track|create|detonate$)}/{id:int}");
routeBuilder.MapGet("hello/{name}", context =>
{
var name = context.GetRouteValue("name");
// This is the route handler when HTTP GET "hello/<anything>" matches
// To match HTTP GET "hello/<anything>/<anything>,
// use routeBuilder.MapGet("hello/{*name}"
return context.Response.WriteAsync($"Hi, {name}!");
});
var routes = routeBuilder.Build();
app.UseRouter(routes);
已给URLs的响应列表。
URI | Response |
---|---|
/package/create/3 | Hello! Route values: [operation, create], [id, 3] |
/package/track/-3 | Hello! Route values: [operation, track], [id, -3] |
/package/track/-3/ | Hello! Route values: [operation, track], [id, -3] |
/package/track/ | <未通过,没有匹配> |
GET /hello/Joe | Hi, Joe! |
POST /hello/Joe | <未通过, 只匹配GET请求> |
GET /hello/Joe/Smith | <未通过,没有匹配> |
如果你只配置了一个路由,调用 app.UseRouter
方法,并传递一个IRouter实例。而不必调用 RouteBuilder
。
框架提供了一系列的创建路由的扩展方法,比如:
一些像 MapGet
这样的方法,需要提供一个 RequestDelegate 。 请求委托
在路由匹配的时候将被用做 路由处理程序 。这个系列的其他方法运行配置一个中间件管道来用做路由处理程序。如果 Map 方法没有接受到一个处理程序,例如 MapRoute
,那么它会调用 DefaultHandler。
Map[Verb]
方法在方法名中使用约束限制路由的请求方式。例如 MapGet 和 MapVerb。
路由模板参考
令牌内的大括号( { }
)定义了路由值 参数的边界。你可以在一个路由段中定义多个路由值参数,但它们必须用文字值分开,例如 {controller=Home}{action=Index}
不是一个有效路由,因为在 {controller}
和 {action}
之间没有文字值。这些路由值参数必须有一个名称,并可以有附加指定的属性。
除了路由参数(例如, {id}
)和路径符合 /
之外的文本必须匹配URL中的值。文本匹配是大小写敏感的且基于解码后的URLs路径。如果要匹配路由参数分隔符 {
或 }
,重复( {{
或 }}
)即可。
试图捕获一个带有可选扩展名的文件名的 URL 模式还需要有其他考虑。例如,使用模板 files/{filename}.{ext?}
- 当文件名和扩展名都存在的时候,这两个值会被填充。如果在URL中只存在文件名,因为点号 .
是可选的路由也会匹配。下面的 URLs 将会匹配这个路由。
/files/myFile.txt
/files/myFile.
/files/myFile
你可以使用 *
号作为一个路由参数的前缀去绑定其余的 URI - 这被叫做全捕获参数。例如, blog/{*slug}
将会匹配任何以 /blog
开头且有任何值跟随(对应到 slug
路由值)的 URI。全捕获型参数也能匹配空字符串。
路由参数可以有默认值,定义的方式是在参数名称后定义默认值,用 =
号分开。例如, {controller=Home}
将 Home
作为 controller
的默认值。如果在URL中这个参数没有值就将使用默认值。此外对于默认值,路由参数是可选的(通过在参数名称后定义加一个 ?
,比如 id?
)。在参数可选和拥有默认值之间的区别就是拥有默认值的路由参数总是会参数一个值;而可选的参数只有URL提供值了才会有值。
路由参数也可以有约束,必须匹配从URL绑定的路由值。通过在路由参数名后增加一个冒号 :
和约束名来定义一个内联约束。如果内联约束需要参数,需要在约束名称后用括号括起来提供。多个内联约束可以通过附加另一个冒号 :
和约束名来定义。为在URL处理中使用 IRouteConstraint 实例,需要把约束名称传递到 IInlineConstraintResolver 服务来创建一个。
下面的列表展示了一些路由模板和他们的行为。
路由模板 | 匹配URL示例 | 注释 |
---|---|---|
hello | /hello | 将只匹配'/hello' 路径 |
{Page=Home} | / | 将匹配且设置 Page 为 Home 。 |
{Page=Home} | /Contact | 将匹配且设置 Page 为 Contact 。 |
{controller}/{action}/{id?} | /Products/List | 会映射到 Products 控制器的 List 方法。 |
{controller}/{action}/{id?} | /Products/Details/123 | 会映射到 Products 控制器的 Details 方法,且 id 的值为 123 。 |
{controller=Home}/ {action=Index}/{id?} | / | 会映射到 Home 控制器的 Index 方法; id 忽略掉。 |
大体来说,设置路由的最简单的方法是使用模板。约束和默认值可以在路由模板之外定义。
建议
打开 logging 去查看内置路由的实现方式, 例如Route
类, 如何匹配请求。
路由约束参考
当一个路由已经匹配到了传入的URL并标记了URL中的路由值时路由约束就会执行。它一般会检查和路由模板相关的路由的值而且会做出一个关于这个值是否是可接受的简单决定。有些路由约束使用路由值之外的数据来决定该请求是否可以进行路由。例如, HttpMethodRouteConstraint 可以接受或拒绝一个基于其Hppt的请求。
警告
避免使用约束来做验证,这样做意味着非法输入会得到一个404(没有找到)的结果。而不是一个400的状态码加上合适的错误信息。路由约束应该用来在路由间做区分,而不是为了特定的路由做验证。
下表展示了一些路由约束和他们的预期行为。
约束 | 示例 | 匹配示例 | 注释 |
---|---|---|---|
int | {id:int} | 123 | 匹配所有整型 |
bool | {active:bool} | true | 匹配 true 或 false |
datetime | {dob:datetime} | 2016-01-01 | 匹配一个合法的 DateTime 值 (固定区域性 - 请看 options) |
decimal | {price:decimal} | 49.99 | 匹配一个合法的 decimal 值 |
double | {weight:double} | 4.234 | 匹配一个合法的 double 值 |
float | {weight:float} | 3.14 | 匹配一个合法的 float 值 |
guid | {id:guid} | 7342570B- | 匹配一个合法的 Guid 值 |
long | {ticks:long} | 123456789 | 匹配一个合法的 long 值 |
minlength(value) | {username:minlength(5)} | steve | 至少5个字符串长。 |
maxlength(value) | {filename:maxlength(8)} | somefile | 字符串不能超过8个字符长。 |
length(min,max) | {filename:length(4,16)} | Somefile.txt | 字符串至少8个长度且不超过16个字符长度。 |
min(value) | {age:min(18)} | 19 | 值至少是18。 |
max(value) | {age:max(120)} | 91 | 值不能超过120。 |
range(min,max) | {age:range(18,120)} | 91 | 值必须介于18和120之间。 |
alpha | {name:alpha} | Steve | 字符串必须是由字母字符组成。 |
regex(expression) | {ssn:regex(^d{3}-d{2}-d{4}$)} | 123-45-6789 | 字符串必须匹配提供的正则表达式。 |
required | {name:required} | Steve | 用于在URL生成时强制必须存在值。 |
注意
验证URL可转换为CLR类型(例如int或DateTime)的路由约束总是使用固定区域性 - 它们认为URL是不可本地化的。框架提供的路由约束不会修改路由值。从URL解析过来的所有路由值都会存为字符串。例如,浮点路由约束 会试图将路由值转换为一个浮点型,但转换后的值只用于验证它是否能够转换为浮点型。
建议
为约束一个参数是一组列可能的值,你可以使用正则表达式(例如{action:regex(list|get|create)}
)。这将只匹配action
的值是list
、get
或create
。 如果将 "list|get|create" 传入约束字典,是等价的。传入约束字典的约束(没有内联模板),没有匹配到已知的约束也会被视为正则表达式。
URL生成参考
下面的例子展示了在给定一个路由值字典和一个 路由集合
的情况下如何生成一个链接。
app.Run(async (context) =>
{
var dictionary = new RouteValueDictionary
{
{ "operation", "create" },
{ "id", 123}
};
var vpc = new VirtualPathContext(context, null, dictionary, "Track Package Route");
var path = routes.GetVirtualPath(vpc).VirtualPath;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("Menu<hr/>");
await context.Response.WriteAsync($"<a href='{path}'>Create Package 123</a><br/>");
});
上面示例最终生成的 相对路径
是 /package/create/123
。
VirtualPathContext
构造函数的第二个参数是一个 ambient值
的集合。通过限制开发人员必须在特定请求上下文中定义值的数量,环境值提供了方便。当前请求的路由值被当做生成链接的环境值。例如,在MVC应用中,如果正在Home控制器的About方法中,链接到Index方法时你不需要定义控制器路由值(Home的环境值将会被使用)。
没有匹配到参数的环境值将被忽略,同样有明确保留的值覆盖它,按照URL中从做到右的顺序,环境值也会被忽略。
显示提供的但没有匹配的值将会被加到查询字符串中。下表展示了使用路由模板 {controller}/{action}/{id?}
的结果
环境值 | 明确值 | 结果 |
---|---|---|
controller="Home" | action="About" | /Home/About |
controller="Home" | controller="Order",action="About" | /Order/About |
controller="Home",color="Red" | action="About" | /Home/About |
controller="Home" | action="About",color="Red" | /Home/About?color=Red |
如果一个路由有一个没有匹配到参数的默认值,而且这个值被提供了,那它必须匹配这个默认值。例如:
routes.MapRoute("blog_route", "blog/{*slug}",
defaults: new { controller = "Blog", action = "ReadPost" });
当提供了controller和action的匹配值,才能生成这个路由的链接。