【.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


作者:Garfield

同步更新至個人博客:http://www.randyfield.cn/

本文版權歸作者所有,未經許可禁止轉載,否則保留追究法律責任的權利,若有需要請聯系287572291@qq.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM