路由在任何一门编程语言的web框架中,都是一个重点,只有知道路由规则,才能通过URL映射服务端的请求处理。本篇描述的路由系统.netcore版本是.net core 3.x。

1.路由

将用户请求地址=>映射为一个请求处理器

  • 委托:Func<HttpContext,Task>
  • MVC:any controller any action

路由负责匹配传入的 HTTP 请求,然后将这些请求发送到应用的可执行终结点。

1.1 终结点-EndPoint

一个终结点(EndPoint)就是一个处理请求的委托。终结点是一个抽象概念,不止服务于常见的mvc模式。

1.2 原理

  • 1.【定义EndPoints】:在程序启动前应该定义好程序中有哪些终结点,针对mvc来说的话可以自动将程序中与路由匹配的action转换成对应的终结点,其它框架应该也有对应的方式,反正最终我们所有用来处理请求的东东都变成了终结点。这步是在定义路由时自动完成的。
  • 2.【定义Urls与EndPoints的对应关系】:除了定义终结点我们还要定义 请求路径终结点的对应关系,请求抵达时才能匹配找到合适的终结点来处理我们的请求,这步相当于定义路由
  • 3.【解析Url->EndPoint】:定义一个解析器,当请求抵达时根据终结点与路径的对应关系找到终结点,微软已定义好对应的中间件来表示这个解析器。
  • 4.【EndPoint->委托】:最后需要定义一个中间件,在上面的中间件执行后,就可以拿到与当前请求匹配的终结点,最终调用它的委托处理请求,这个中间件就是mvc中间件
  • 5.【3-4之间】:到此asp.net core 3.x的中间件路由默认差不多就这样了,此时可以定义自己的中间件,放在步骤3后面,拿到终结点做一些高级处理。微软定义的一些中间件也是这个套路。

2.路由基础

在所有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

  • UseRouting:向中间件管道添加路由匹配。 此中间件会查看应用中已经定义的终结点集,并根据请求选择最佳匹配。
  • UseEndpoints:向中间件管道添加终结点执行。 它会运行与所选终结点关联的委托。

再说说MapGet:

  • http请求,get / =>将会执行后面的委托
  • 如果请求方法不是GET,或者URL不是/,则找不到路由匹配,就会返回著名的404

2.1 再看终结点

MapGet就算是定义了一个终结点,一个终结点具有以下内容:

  • 选择:通过匹配 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 链接到此调用会将授权策略附加到该终结点。
  • 调用 UseAuthenticationUseAuthorization 会添加身份验证和授权中间件。 这些中间件位于 UseRoutingUseEndpoints 之间,因此它们可以:
    • 查看 UseRouting 选择的终结点。
    • UseEndpoints 发送到终结点之前应用授权策略。

2.2 终结点元数据

上面的代码有两个终结点,其中只有一个终结点附加了授权策略。MapHealthChecks,如果请求healthz,就会授权检查。说明,终结点可以附加额外的数据,称为元数据

  • 元数据可以通过routing-aware中间件处理
  • 元数据可以是.net的任意类型

3.Netcore 3.x中的路由概念

通过上面的基础路由,我们可以看到,路由系统通过强大的终结点概念构建在中间件管道之上。

3.1 终结点定义

  • 可执行:RequestDelegate
  • 可扩展:元数据集合
  • Selectable:选择性包含路由信息
  • 可枚举:可以通过DI检索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
  • UseRoutingUseEndpoints之间的中间件:执行终结点前处理路由结果

    • 通常会检查元数据以了解终结点。
    • 通常会根据 UseAuthorizationUseCors 做出安全决策。
    • The combination of middleware and metadata allows configuring policies per-endpoint.

4.终端中间件与路由

查看如下代码:

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的中间件

  • 都允许终结处理管道
    • 终端中间件:return终结
    • 终结点(Endpoint):直接就是终结
  • 位置
    • 终端中间件:任意任意放置
    • 终结点(Endpoint):在UseEndpoints中执行
  • 匹配
    • 中间件:允许任意代码确定中间件匹配
    • 自定义路由匹配可能比较复杂,且难以匹配
    • 自带路由为典型应用提供了简单的解决方案。 大多数应用不需要自定义路由匹配代码。
  • 带有中间件的终结点,例如 UseAuthorizationUseCors.
    • 通过 UseAuthorizationUseCors 使用终端中间件需要与授权系统进行手动交互。

使用场景

  • 终结点:
    • 处理请求的委托
    • 任意元数据的集合。 元数据用于实现横切关注点,该实现基于附加到每个终结点的策略和配置。
  • 终端中间件
    • 大量的编码和测试
    • 手动与其他系统集成,实现灵活性

5.URL匹配

一句话:通过url找到委托

当路由中间件执行时,从当前请求路由到 HttpContext上的请求功能,它会设置 Endpoint和路由值(Route Values):

  • 调用 GetEndpoint 获取终结点。
  • HttpRequest.RouteValues 将获取路由值的集合。

6.路由约束

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},等等,更多请参考官方说明

关于路由约束还有正则表达式,自定义路由约束等等内容,但是其实并不常用,更多内容请阅读微软官方文档

7.Asp.net core 3.x中的路由

ASP.NET Core控制器使用的是Routing 中间件去匹配请求的URL并将其映射至Actions

一般会有一个路由模板:

  • Startup中,或者attributes
  • 描述URL路径如何匹配至控制器中的action
  • 以及在基础路由中的生成URL链接。你使用了URL生成,那么生成的URL就会是这个路由模板的样式。

Action匹配,要么是常规路由(Conventionally-routed),要么是属性路由(attributes-routed)

7.1 常规路由

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();

此方法与上面等价

7.2 属性路由

REST API,微软建议使用属性路由。具体也是跟ASP.NET Web API 2 中的属性路由差不了太多,其中的细节与技巧留着以后总结吧,多则惑少则得

 app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

路由通过UseRoutingUseEndpoints中间件注册。如果要使用控制器(别忘了,你是可以自定义路由的):

  • UseEndpoints中调用MapControllers,映射属性路由控制器
  • 调用MapControllerRouteMapAreaControllerRoute映射常规路由控制器

7.3.自定义路由

如上所述,endpoints.MapControllerRoute()endpoints.MapControllers();都是微软为开发者行的方便,将用户请求地址=>匹配到MVC的控制器Controller与Action。那我们是完全可以摒弃微软MVC模式下那一套路由法则,走自定义路由,更自由,更具有掌控力,这里主要利用中间件来实现,也就是上面说的那些在匹配url的中间件。这也是Koa.jsGin等不同语言的下的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");
});
  • 1.应用级中间件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");
});
  • 2.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.UseEndpointsMapGet
 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