zoukankan      html  css  js  c++  java
  • 【.Net core】ASP.NET Core 中的路由

    路由在任何一门编程语言的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()
    }
    

    好了路由的内容就讲到这儿,微软有时候是封装的太好了,太优雅,但是我们还是要去探究一下,所谓知其然还要知其所以然。

    8.参考链接

    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

  • 相关阅读:
    从零开始学习SSH框架笔记之一 初识SSH框架及下载地址
    CentOS 6.0 升级内核步骤、方法
    解决Ubuntu详细信息中图形显示为未知的情况
    【Oracle XE系列之二】Oracle XE创建表空间、创建用户以及授权、查看权限
    排序算法大总结
    我的vimrc 设置
    你的c++学的怎么样?
    腾讯的笔试题一道
    一道递归算法题
    vc6 bug真多 写c++别用vc6
  • 原文地址:https://www.cnblogs.com/RandyField/p/12770992.html
Copyright © 2011-2022 走看看