路由在任何一门编程语言的web框架中,都是一个重点,只有知道路由规则,才能通过URL映射服务端的请求处理。本篇描述的路由系统.netcore版本是.net core 3.x。
将用户请求地址=>映射为一个请求处理器
Func<HttpContext,Task>
any controller any action
路由负责匹配传入的 HTTP 请求,然后将这些请求发送到应用的可执行终结点。
一个终结点(EndPoint)就是一个处理请求的委托。终结点是一个抽象概念,不止服务于常见的mvc模式。
请求路径
与 终结点
的对应关系,请求抵达时才能匹配找到合适的终结点来处理我们的请求,这步相当于定义路由。asp.net core 3.x
的中间件路由默认差不多就这样了,此时可以定义自己的中间件,放在步骤3后面,拿到终结点做一些高级处理。微软定义的一些中间件也是这个套路。在所有Asp.net core
的代码中,路由都是在Startup.Configure
中的中间件管道注册
如下代码
app.UseRouting();app.UseEndpoints(endpoints=>{ endpoints.MapGet("/",async context=> { await context.Response.WriteAsync("Hello,World!"); });});
上面的代码像不像Koa.js
router.get("/",async(ctx)=>{ })
好了,说回我们的asp.net core
,这里使用了一对中间件来注册路由:UseRouting
,UseEndpoints
再说说MapGet:
get /
=>将会执行后面的委托GET
,或者URL不是/,则找不到路由匹配,就会返回著名的404MapGet
就算是定义了一个终结点,一个终结点具有以下内容:
url+http
请求类似的,还有Map
,MapPost
,MapPut
,MapDelete
,在ASP.NET Core
同系列的其他框架连接到路由系统,是通过下面的方法:
Razor Pages
是通过MapRazorPages
Controllers
是通过MapControllers
Signal
是通过MapHub<Thb>
gRPC
是通过MapGrpcService
再看下面的代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Matches request to an endpoint. app.UseRouting(); // Endpoint aware middleware. // Middleware can use metadata from the matched endpoint. app.UseAuthentication(); app.UseAuthorization(); // Execute the matched endpoint. app.UseEndpoints(endpoints => { // Configure the Health Check endpoint and require an authorized user. endpoints.MapHealthChecks("/healthz").RequireAuthorization(); // Configure another endpoint, no authorization requirements. endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); });}
上面的代码説明以下几点:
endpoints.MapHealthChecks("/healthz").RequireAuthorization();
MapHealthChecks
调用添加运行状况检查终结点。 将 RequireAuthorization
链接到此调用会将授权策略附加到该终结点。UseAuthentication
和 UseAuthorization
会添加身份验证和授权中间件。 这些中间件位于 UseRouting
和 UseEndpoints
之间,因此它们可以:UseRouting
选择的终结点。UseEndpoints
发送到终结点之前应用授权策略。上面的代码有两个终结点,其中只有一个终结点附加了授权策略。MapHealthChecks
,如果请求healthz
,就会授权检查。说明,终结点可以附加额外的数据,称为元数据
routing-aware
中间件处理通过上面的基础路由,我们可以看到,路由系统通过强大的终结点概念构建在中间件管道之上。
RequestDelegate
EndpointDataSource
列出终结点集合app.UseRouting();app.Use(next => context => { var endpoint = context.GetEndpoint(); if (endpoint is null) { return Task.CompletedTask; } Console.WriteLine($"Endpoint: {endpoint.DisplayName}"); if (endpoint is RouteEndpoint routeEndpoint) { Console.WriteLine("Endpoint has route pattern: " + routeEndpoint.RoutePattern.RawText); } foreach (var metadata in endpoint.Metadata) { Console.WriteLine($"Endpoint has metadata: {metadata}"); } return Task.CompletedTask; });app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); });
注意看上面的代码
RouteEndpoint
context.GetEndpoint();
通过这句,就能检索终结点,调用RequestDelegate
UseRouting
中间件使用 SetEndpoint方法
将终结点附加到当前上下文。 可以将 UseRouting
中间件替换为自定义逻辑,同时仍可获得使用终结点的益处。 终结点是中间件等低级别基元,不与路由实现耦合。 大多数应用都不需要将 UseRouting
替换为自定义逻辑。说白了,路由是可以自定义的。
在UseRouting
之前的中间件:修改请求的属性
UseRewriter
UseHttpMethodOverride
UsePathBase
在UseRouting
与UseEndpoints
之间的中间件:执行终结点前处理路由结果
UseAuthorization
和 UseCors
做出安全决策。查看如下代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Approach 1: Writing a terminal middleware. app.Use(next => async context => { if (context.Request.Path == "/") { await context.Response.WriteAsync("Hello terminal middleware!"); return; } await next(context); }); app.UseRouting(); app.UseEndpoints(endpoints => { // Approach 2: Using routing. endpoints.MapGet("/Movie", async context => { await context.Response.WriteAsync("Hello routing!"); }); });}
何为终端中间件?终端中间件:在匹配url
的中间件
UseEndpoints
中执行UseAuthorization
和UseCors
.UseAuthorization
或 UseCors
使用终端中间件需要与授权系统进行手动交互。使用场景
一句话:通过url
找到委托
当路由中间件执行时,从当前请求路由到 HttpContext
上的请求功能,它会设置 Endpoint
和路由值(Route Values):
GetEndpoint
获取终结点。HttpRequest.RouteValues
将获取路由值的集合。app.UseEndpoints(endpoints =>{ endpoints.MapGet("/hello/{name:alpha}", async context => { var name = context.Request.RouteValues["name"]; await context.Response.WriteAsync($"Hello {name}!"); });});
常见的路由约束{id:int}``{name:alpha}``{active:bool}
,等等,更多请参考官方说明
关于路由约束还有正则表达式,自定义路由约束等等内容,但是其实并不常用,更多内容请阅读微软官方文档
ASP.NET Core控制器使用的是Routing 中间件去匹配请求的URL并将其映射至Actions
一般会有一个路由模板:
Startup
中,或者attributes
Action匹配,要么是常规路由(Conventionally-routed),要么是属性路由(attributes-routed)
Startup.Configure
app.UseEndpoints(endpoints =>{ endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");});
MVC的控制器-视图应用,基本都是使用上面的路由模板。
大概:URL 路径/
使用路由模板默认Home
控制器和Index
操作。 URL 路径/Home
使用路由模板默认Index
操作,这个跟以前的ASP.NET 4.x
,是一样,不赘述了。
简便方法:
endpoints.MapDefaultControllerRoute();
此方法与上面等价
REST API,微软建议使用属性路由。具体也是跟ASP.NET Web API 2
中的属性路由差不了太多,其中的细节与技巧留着以后总结吧,多则惑少则多。
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
路由通过UseRouting
和UseEndpoints
中间件注册。如果要使用控制器(别忘了,你是可以自定义路由的):
UseEndpoints
中调用MapControllers
,映射属性路由控制器MapControllerRoute
和 MapAreaControllerRoute
映射常规路由控制器如上所述,endpoints.MapControllerRoute()
与endpoints.MapControllers();
都是微软为开发者行的方便,将用户请求地址=>匹配到MVC的控制器(Controller)与Action。那我们是完全摒弃微软MVC模式下那一套路由法则,走自定义路由,这里主要利用中间件来实现,也就是上面说的那些在匹配url
的中间件。这也是Koa.js
和Gin
等不同语言的下的web框架实现http请求路由匹配内部方法。详情请阅读【对比学习】Koa.js、Gin与Asp.net core-中间件
在koa.js
的中间件分类
app.use()
router.get(‘/news‘,async(ctx,next)=>{await next();})
那么类比,asp.net core
也是可以这样来看
app.Use(async(context,next)=>{ await context.Response.WriteAsync("match everything");});
app.Use()
,短路一切,任何路由都会只返回match everything
,后面再多中间件都不会执行,如果想继续匹配就需要next
app.Use(async(context,next)=>{ await context.Response.WriteAsync("first"); await next();//await next.Invoke(); });app.Use(async(context,next)=>{ await context.Response.WriteAsync("second");});
Run()
:只有一个RequestDelegate
委托,参数只有HttpContext
,没有next
所以Run()
自然能作为一个终结点.app.Run(async context =>{ await context.Response.WriteAsync("first");});
也正是因为RequestDelegate
,所以app.Run()
作为终结点的委托。
app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { var endpoint = context.GetEndpoint(); await context.Response.WriteAsync("/"); }); });
app.UseEndpoints
与MapGet
app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { var endpoint = context.GetEndpoint(); await context.Response.WriteAsync("Hello World!"); }); endpoints.MapGet("/test1", async context => { var endpoint = context.GetEndpoint(); await context.Response.WriteAsync("test1"); }); });
app.Map()
app.Map("/map", app => { app.Run(async context => { await context.Response.WriteAsync("我是map"); }); });
微软官方把这个叫中间件管道分支,博主认为这还是可以作为自定义路由的方式来看待。
public class Startup{ public void Configure(IApplicationBuilder app) { app.Map("/user/login", HandleLogin); } private static void HandleLogin(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("登录成功"); }); }}
app.Map("/user", level1App => { level1App.Map("/login", level2AApp => { // "/user/login" processing }); level1App.Map("/info", level2BApp => { // "/user/user" processing });});
Gin
里面也有类似的,叫路由组
func main() { r := gin.Default() userGroup := r.Group("/user") { userGroup.GET("/index", func(c *gin.Context) {...}) userGroup.GET("/login", func(c *gin.Context) {...}) userGroup.POST("/login", func(c *gin.Context) {...}) } shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {...}) shopGroup.GET("/cart", func(c *gin.Context) {...}) shopGroup.POST("/checkout", func(c *gin.Context) {...}) // 嵌套路由组 xx := shopGroup.Group("xx") xx.GET("/oo", func(c *gin.Context) {...}) } r.Run()}
好了路由的内容就讲到这儿,微软有时候是封装的太好了,太优雅,但是我们还是要去探究一下,所谓知其然还要知其所以然。
https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-3.1
https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.1
https://www.cnblogs.com/jionsoft/archive/2019/12/29/12115417.html
https://godoc.org/github.com/gin-gonic/gin#RouterGroup