了解ASP.NET Core端点路由

原作者Areg Sarkissian

介绍

在这篇文章中,我将说明从版本2.2开始已添加到ASP.NET Core中间件管道中的新的端点路由功能,以及它如何演进到当前在预览版3的即将发布的版本3.0。

端点路由背后的动机

在端点路由之前,在HTTP请求处理管道的末尾,在ASP.NET Core MVC中间件中完成了ASP.NET Core应用程序的路由解析。这意味着在中间件管道中的MVC中间件之前,路由信息(例如将执行哪些控制器操作)对于处理请求的中间件不可用。

例如在CORS或授权中间件中提供此路由信息特别有用,以将该信息用作授权过程中的一个因素。

端点路由还允许我们将路由匹配逻辑与MVC中间件解耦,然​​后将其移动到其自己的中间件中。它允许MVC中间件专注于其将请求分发到由端点路由中间件解决的特定控制器操作方法的责任。

新的端点路由中间件

由于上述原因,端点路由功能的诞生是为了使路由解析能够在单独的端点路由中间件中的管道中更早地发生。可以在管道中的任何位置放置此新的中间件,之后管道中的其他中间件可以访问已解析的路由数据。

端点路由中间件API随即将发布的.NET Core框架3.0版本一起发展。因此,我将在以下各节中描述的API可能不是该功能的最终版本。但是,总体概念和对如何使用端点路由进行路由解析和调度的理解仍然应该适用。

在以下各节中,我将引导您完成从2.2版到3.0版Preview 3的端点路由实现的当前迭代,然后我将注意到基于当前ASP.NET Core源代码的一些更改。

端点路由涉及的三个核心概念

您需要了解三个总体概念,才能理解端点路由的工作方式。

这些是以下内容:

  • 端点路由解析
  • 端点派遣
  • 端点路由映射

端点路由解析

端点路由解析是查看传入请求并将请求使用路由映射映射到端点的概念。端点表示传入请求解析的控制器操作,以及附加到与请求匹配的路由的其他元数据。

路由解析中间件的工作是使用基于路由映射解析的路由中的路由信息来构造Endpoint对象。然后,中间件将该对象放置在http上下文中,在该上下文中,在管道中的端点路由中间件可以访问端点对象并使用其中的路由信息​​之后出现的其他中间件。

在端点路由之前,路由解析是在中间件管道末端的MVC中间件中完成的。该框架的当前2.2版本添加了一个新的端点路由解析中间件,该中间件可以放置在管道中的任何位置,但是将端点分发保留在MVC中间件中。这将在3.0版本中发生变化,在该版本中,终结点调度将在单独的终结点调度中间件中进行,该中间件将替换MVC中间件。

端点派遣

端点调度是调用控制器操作方法的过程,该方法对应于由端点路由中间件解析的端点。

端点分派中间件是管道中的最后一个中间件,它从http上下文中获取端点对象,并分派给解析的端点指定的特定控制器操作。

当前,在2.2版中,在管道末端的MVC中间件中完成对action方法的调度。

在3.0版预览3中,删除了MVC中间件。相反,默认情况下,端点调度发生在中间件管道的末尾。由于已删除MVC中间件,因此通常传递给MVC中间件的路由映射配置将传递给端点路由解析中间件。

根据当前的源代码,即将发布的3.0最终版本应该在管道的末尾放置一个新的端点路由中间件,以使端点再次显式分派。路由映射配置将传递到此新的中间件,而不是版本3预览版3中的端点路由解析中间件。

端点路由映射

定义路由中间件时,我们可以选择传入一个lambda函数,该函数包含的路由映射将覆盖ASP.NET Core MVC中间件扩展方法指定的默认路由映射。

路由解析过程使用路由映射将传入的请求参数与路由映射中指定的路由进行匹配。

使用新的端点路由功能,ASP.NET Core团队必须决定应使用哪个中间件(端点解析或端点调度中间件)获取路由映射配置lambda作为参数。

实际上,这是API不断变化的端点路由的一部分。在撰写本文时,路由映射已从路由解析中间件移至端点调度程序中间件。

我将首先在版本3预览3中向您展示路由映射API,然后在ASP.NET Core源代码中向您展示最新的路由映射API。在源代码版本中,我们将看到路由映射已移至端点调度程序中间件扩展方法。

重要的是要注意,在应用程序启动配置期间设置路由映射之后,端点解析会在运行时请求处理期间发生。因此,路由解析中间件可以在请求处理期间访问路由映射,而不管路由映射配置将传递到哪个中间件。

访问已解析的端点

端点路由解析中间件之后的任何中间件都将能够通过HttpContext访问已解析的端点。

以下代码段显示了如何在自己的中间件中完成此操作:

//our custom middleware
app.Use((context, next) =>
{
    var endpointFeature = context.Features[typeof(Microsoft.AspNetCore.Http.Features.IEndpointFeature)]
                                           as Microsoft.AspNetCore.Http.Features.IEndpointFeature;

    Microsoft.AspNetCore.Http.Endpoint endpoint = endpointFeature?.Endpoint;

    //Note: endpoint will be null, if there was no
    //route match found for the request by the endpoint route resolver middleware
    if (endpoint != null)
    {
        var routePattern = (endpoint as Microsoft.AspNetCore.Routing.RouteEndpoint)?.RoutePattern
                                                                                   ?.RawText;

        Console.WriteLine("Name: " + endpoint.DisplayName);
        Console.WriteLine($"Route Pattern: {routePattern}");
        Console.WriteLine("Metadata Types: " + string.Join(", ", endpoint.Metadata));
    }
    return next();
});

如您所见,我正在通过IEndpointFeature或Http Context访问已解析的终结点对象。该框架提供了包装器方法来访问终结点对象,而不必直接进入上下文,如我在此所示。

端点路由配置

中间件管道终结点路由解析器中间件,终结点调度程序中间件和终结点路由映射lambda是通过ASP.NET Core项目文件Startup.Configure方法设置的Startup.cs

此配置在2.2和3.0 Preview 3版本之间进行了更改,并且在3.0发布版本之前仍在更改。因此,为了演示端点路由配置,我将基于上面列出的三个核心概念将端点路由中间件配置的一般形式声明为伪代码:

//psuedocode that passes route map to endpoint resolver middleware
public void Configure(IApplicationBuilder app
                     , IHostingEnvironment env)
{
    //middleware configured before the UseEndpointRouteResolverMiddleware middleware
    //that does not have access to the endpoint object
    app.UseBeforeEndpointResolutionMiddleware();

    //middleware that inspects the incoming request, resolves a match to the route map
    //and stores the resolved endpoint object into the httpcontext
    app.UseEndpointRouteResolverMiddleware(routes =>
    {
        //This is the route mapping configuration passed to the endpoint resolver middleware
        routes.MapControllers();
    })

    //middleware after configured after the UseEndpointRouteResolverMiddleware middleware
    //that can access to the endpoint object
    app.UseAfterEndpointResolutionMiddleware();

    //The middleware at the end of the pipeline that dispatches the controler action method
    //will replace the current MVC middleware
    app.UseEndpointDispatcherMiddleware();
}

此版本的伪代码显示了作为参数传递给UseEndpointRouteResolverMiddleware端点路由解析中间件扩展方法的路由映射lambda 

匹配当前源代码的替代版本如下所示:

//psuedocode version 2 that passes route map to endpoint dispatch middleware
public void Configure(IApplicationBuilder app
                     , IHostingEnvironment env)
{
    app.UseBeforeEndpointResolutionMiddleware()

    //This is the endpoint route resolver middleware
    app.UseEndpointRouteResolverMiddleware();

    //This middleware can access the resolved endpoint object via HttpContext
    app.UseAfterEndpointResolutionMiddleware();

    //This is the endpoint dispatch middleware
    app.UseEndpointDispatcherMiddleware(routes =>
    {
        //This is the route mapping configuration passed to the endpoint dispatch middleware
        routes.MapControllers();
    });
}

在此版本中,路由映射配置作为参数传递给端点调度中间件扩展方法UseEndpointDispatcherMiddleware

无论哪种方式,UseEndpointRouteResolverMiddleware()端点解析程序中间件都可以在请求处理时访问路由映射以进行路由匹配。

一旦路由与UseEndpointRouteResolverMiddlewareEndpoint对象匹配,便将使用route参数构造并设置为httpcontext,以便后续管道中的中间件可以访问Endpoint对象并在需要时使用它。

在此伪代码的版本3预览3版本中,路由映射被传递到,UseEndpointRouteResolverMiddleware并且UseEndpointDispatcherMiddleware在管道的末尾不存在这是因为在此版本中,ASP.NET框架本身隐式在请求管道的末尾分派了已解析的终结点。

因此,表示版本3预览版3的伪代码具有以下形式:

//pseudo code representing v3 preview 3 endpoint routing API 
public void Configure(IApplicationBuilder app
                     , IHostingEnvironment env)
{
    app.UseBeforeEndpointResolutionMiddleware()

    //This is the endpoint route resolver middleware
    app.UseEndpointRouteResolverMiddleware(routes =>
    {
        //The route mapping configuration is passed to the endpoint resolution middleware
        routes.MapControllers();
    })

    //This middleware can access the resolved endpoint object via HttpContext
    app.UseAfterEndpointResolutionMiddleware()

    // The resolved endpoint is implicitly dispatched here at the end of the pipeline
    // and so there is no explicit call to a UseEndpointDispatcherMiddleware
}

该API似乎随着3.0发行版的变化而变化,因为当前的源代码显示,该API UseEndpointDispatcherMiddleware已重新添加,并且中间件将路由映射作为参数,如上面的第二版伪代码所示。

2.2版中的端点路由

如果使用.NET Core SDK版本2.2创建Web API项目,则该Startup.Configure方法中将显示以下代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    //By default endpoint routing is not added
    //the MVC middleware dispatches the controller action
    //and the MVC middleware configures the default route mapping
    app.UseMvc();
}

使用UseMvc()扩展方法在中间件流水线的末尾配置MVC中间件此方法在启动配置时在内部设置默认的MVC路由映射配置,并在请求处理期间调度控制器操作。

默认情况下,v2.2中的即用型模板仅配置MVC调度程序中间件。这样,MVC中间件还根据路由映射配置和传入的请求数据来处理路由解析。

但是,我们可以使用一些其他配置来添加“端点路由”,如下所示:


using Microsoft.AspNetCore.Internal;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    //added endpoint routing that will resolve the endpoint object
    app.UseEndpointRouting();

    //middleware below will have access to the Endpoint

    app.UseHttpsRedirection();

    //the MVC middleware dispatches the controller action
    //and the MVC middleware configures the default route mapping
    app.UseMvc();
}

在这里,我们添加了命名空间Microsoft.AspNetCore.Internal包括它在内,可以启用一种附加的IApplicationBuilder扩展方法UseEndpointRouting,该方法是解决路由并将端点对象添加到httpcontext的端点解析中间件。

您可以在以下位置查看UseEndpointRouting版本2.2中扩展方法的源代码

https://github.com/aspnet/AspNetCore/blob/v2.2.4/src/Http/Routing/src/Internal/EndpointRoutingApplicationBuilderExtensions.cs

在版本2.2中,管道末端的MVC中间件充当端点调度程序中间件。它将已解析的端点分派给适当的控制器操作。

端点解析中间件使用由MVC中间件配置的路由映射。

启用端点路由后,如果添加以下自己的自定义中间件,则可以实际检查已解析的端点对象:

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseEndpointRouting();

    app.UseHttpsRedirection();

    //our custom middlware
    app.Use((context, next) =>
    {
        var endpointFeature = context.Features[typeof(IEndpointFeature)] as IEndpointFeature;
        var endpoint = endpointFeature?.Endpoint;

        //note: endpoint will be null, if there was no resolved route
        if (endpoint != null)
        {
            var routePattern = (endpoint as RouteEndpoint)?.RoutePattern
                                                          ?.RawText;

            Console.WriteLine("Name: " + endpoint.DisplayName);
            Console.WriteLine($"Route Pattern: {routePattern}");
            Console.WriteLine("Metadata Types: " + string.Join(", ", endpoint.Metadata));
        }
        return next();
    });

    app.UseMvc();
}

如您所见,我们可以检查并打印出端点路由解析中间件UseEndpointRouting已解决的端点对象 如果解析器无法将请求匹配到映射的路由,则端点对象将为null。我们需要引入另外两个名称空间来访问端点路由功能。

版本3预览3中的端点路由

在版本3预览3中,端点路由将成为ASP.NET Core的完整公民,并且我们最终将在MVC控制器动作分派器和路由解析中间件之间实现分离。

这是版本3预览版3中的端点启动配置。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    app.UseRouting(routes =>
    {
        routes.MapControllers();
    });

    app.UseAuthorization();

    //No need to have a dispatcher middleware here.
    //The resolved endpoint is automatically dispatched to a controller action at the end
    //of the middleware pipeline
    //If an endpoint was not able to be resolved, a 404 not found is returned at the end
    //of the middleware pipeline
}

如您所见,我们有一种app.UseRouting()配置端点路由解析中间件的方法。该方法还采用匿名lambda函数,该函数配置路由解析器中间件将用来解析传入请求端点的路由映射。

routes.MapControllers()映射函数内部配置默认的MVC路线。

您还将注意到,在此app.UseAuthorization()之后app.UseRouting()配置授权中间件。该中间件将有权访问由端点路由中间件设置的httpcontext端点对象。

注意,在所有其他中间件配置之后,在方法末尾我们没有配置任何MVC或端点调度中间件。

这是因为版本3预览版3的行为是,解析的终结点将由框架本身隐式分派给控制器操作。

与我们针对2.2版所做的类似,我们可以添加相同的自定义中间件来检查已解析的终结点对象。

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    app.UseRouting(routes =>
    {
        routes.MapControllers();
    });

    app.UseAuthorization();

    //our custom middleware
    app.Use((context, next) =>
    {
        var endpointFeature = context.Features[typeof(IEndpointFeature)] as IEndpointFeature;
        var endpoint = endpointFeature?.Endpoint;

        //note: endpoint will be null, if there was no
        //route match found for the request by the endpoint route resolver middleware
        if (endpoint != null)
        {
            var routePattern = (endpoint as RouteEndpoint)?.RoutePattern
                                                          ?.RawText;

            Console.WriteLine("Name: " + endpoint.DisplayName);
            Console.WriteLine($"Route Pattern: {routePattern}");
            Console.WriteLine("Metadata Types: " + string.Join(", ", endpoint.Metadata));
        }
        return next();
    });

    //the endpoint is dispatched by default at the end of the middleware pipeline
}

即将发布的ASP.NET Core 3.0版源代码存储库中的端点路由

当我们接近该框架的3.0版发布时,团队似乎正在通过重新添加对端点分派器中间件配置的调用来使端点路由更加明确。他们还将路由映射配置选项移回了调度程序中间件配置方法。

通过查看当前的源代码,我们可以再次看到这种变化。

以下是来自3.0版示例应用程序音乐商店的源代码的片段:

https://github.com/aspnet/AspNetCore/blob/master/src/MusicStore/samples/MusicStore/Startup.cs

public void Configure(IApplicationBuilder app)
{
    // Configure Session.
    app.UseSession();

    // Add static files to the request pipeline
    app.UseStaticFiles();

    // Add the endpoint routing matcher middleware to the request pipeline
    app.UseRouting();

    // Add cookie-based authentication to the request pipeline
    app.UseAuthentication();

    // Add the authorization middleware to the request pipeline
    app.UseAuthorization();

    // Add endpoints to the request pipeline
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "areaRoute",
            pattern: "{area:exists}/{controller}/{action}",
            defaults: new { action = "Index" });

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action}/{id?}",
            defaults: new { controller = "Home", action = "Index" });

        endpoints.MapControllerRoute(
            name: "api",
            pattern: "{controller}/{id?}");
    });
}

如您所见,我们具有类似于我上面详细介绍的伪代码实现的东西。

特别是,我们仍然具有app.UseRouting()版本3预览版3中的 中间件设置,但是现在,我们还有一个显式的 app.UseEndpoints()终结点分发方法,该方法针对其作用更恰当地命名。

UseEndpoints是一种新的IApplicationBuilder扩展方法,提供了端点调度实现。

您可以在此处查看UseEndpointsUseRouting方法的源代码

https://github.com/aspnet/AspNetCore/blob/master/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs

还要注意,路由映射配置lambda已从UseRouting中间件移至新的UseEndpoints中间件。

UseRouting终点的路线解析器将仍然可以访问的映射来解决在请求处理时间的终点。即使UseEndpoints在启动配置期间将它们传递到中间件。

将端点路由中间件添加到DI容器

要使用端点路由,我们还需要在该Startup.ConfigureServices方法中将中间件添加到DI容器中

2.2版中的ConfigureServices

对于该框架的2.2版,我们需要显式添加对services.AddRouting()下面所示方法的调用,以将端点路由功能添加到DI容器:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting()

    services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

版本3预览3中的ConfigureServices

对于版本3的预览版3框架,端点路由已经在AddMvc()扩展方法的掩盖下的DI容器中进行了配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
            .AddNewtonsoftJson();
}

使用端点路由和路由映射设置端点授权

使用版本3 Preview 3版本时,我们可以将授权元数据附加到端点。我们使用路由映射配置流畅的API RequireAuthorization方法进行此操作。

端点路由解析器在处理请求时将访问此元数据,并将其添加到它在httpcontext上设置的Endpoint对象。

路由解析中间件之后的管道中的任何中间件都可以通过访问已解析的Endpoint对象来访问此授权数据。

特别是授权中间件可以使用此数据来做出授权决策。

当前,路由映射配置参数被传递到端点路由解析器中间件中,但是如前所述,在将来的版本中,路由映射配置将被传递到端点调度器中间件中。

无论哪种方式,附加的授权元数据都将可供端点解析器中间件使用。

以下是版本3预览3 Startup.Configure方法的示例,其中我/secret向端点解析器中间件路由映射配置lambda参数添加了新路由:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    app.UseRouting(routes =>
    {
        routes.MapControllers();

        //Mapped route that gets attached authorization metadata using the RequireAuthorization extension method.
        //This metadata will be added to the resolved endpoint for this route by the endpoint resolver
        //The app.UseAuthorization() middleware later in the pipeline will get the resolved endpoint
        //for the /secret route and use the authorization metadata attached to the endpoint
        routes.MapGet("/secret", context =>
        {
            return context.Response.WriteAsync("secret");
        }).RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin" });
    });

    app.UseAuthentication();

    //the Authorization middleware check the resolved endpoint object
    //to see if it requires authorization. If it does as in the case of
    //the "/secret" route, then it will authorize the route, if it the user is in the admin role
    app.UseAuthorization();

    //the framework implicitly dispatches the endpoint at the end of the pipeline.
}

您可以看到我正在使用该RequireAuthorization方法向路由添加AuthorizeAttribute属性/secret然后,将仅由端点分发发生之前的授权中间件授权以admin角色为用户分发此路由。

正如我在3.3版预览3中所展示的那样,我们可以添加中间件来检查httpcontext中解析的终结点对象,因此我们可以在这里检查添加到终结点元数据中的AuthorizeAttribute:

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    app.UseRouting(routes =>
    {
        routes.MapControllers();

        routes.MapGet("/secret", context =>
        {
            return context.Response.WriteAsync("secret");
        }).RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin" });
    });

    app.UseAuthentication();

    //our custom middleware
    app.Use((context, next) =>
    {
        var endpointFeature = context.Features[typeof(IEndpointFeature)] as IEndpointFeature;
        var endpoint = endpointFeature?.Endpoint;

        //note: endpoint will be null, if there was no
        //route match found for the request by the endpoint route resolver middleware
        if (endpoint != null)
        {
            var routePattern = (endpoint as RouteEndpoint)?.RoutePattern
                                                          ?.RawText;

            Console.WriteLine("Name: " + endpoint.DisplayName);
            Console.WriteLine($"Route Pattern: {routePattern}");
            Console.WriteLine("Metadata Types: " + string.Join(", ", endpoint.Metadata));
        }
        return next();
    });

    app.UseAuthorization();

    //the framework implicitly dispatches the endpoint here.
}

这次,我在授权中间件之前添加了自定义中间件,并引入了两个其他名称空间。

导航到/secret路线并检查元数据,您可以看到它Microsoft.AspNetCore.Authorization.AuthorizeAttribute除了类型外还包含Microsoft.AspNetCore.Routing.HttpMethodMetadata类型。

本文使用的参考

以下文章包含了我用作本文参考的源材料:

https://devblogs.microsoft.com/aspnet/aspnet-core-3-preview-2/

https://www.stevejgordon.co.uk/asp-net-core-first-look-at-global-routing-dispatcher

vesrion 3预览版4的更新

在我发布本文的那段时间,发布了ASP.NET Core 3.0预览版4。Startup.cs创建新的webapi项目时,它添加了我在最新源代码中描述的更改,如以下文件中的代码段所示:

//code from Startup.cs file in a webapi project template

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddNewtonsoftJson();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseHsts();

    app.UseHttpsRedirection();

    //add endpoint resolution middlware
    app.UseRouting();

    app.UseAuthorization();

    //add endpoint dispatch middleware
    app.UseEndpoints(endpoints =>
    {
        //route map configuration
        endpoints.MapControllers();

        //route map I added to show Authorization setup
        endpoints.MapGet("/secret", context =>
        {
            return context.Response.WriteAsync("secret");
        }).RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin" });  
    });
}

如您所见,此版本UseEndpoints在添加端点调度中间件的管道的末尾添加了显式中间件扩展方法。路由配置参数也已从UseRouting预览3中方法移动UseEndpoints预览4中。

结论

端点路由允许ASP.NET Core应用程序在中间件管道的早期确定要调度的端点,以便以后的中间件可以使用该信息来提供当前管道配置无法提供的功能。

这使ASP.NET Core框架更加灵活,因为它使路由匹配和解析功能与终结点调度功能脱钩,而终结点调度功能迄今都与MVC中间件捆绑在一起。

本文作者:作者Areg Sarkissian

翻译至https://aregcode.com/blog/2019/dotnetcore-understanding-aspnet-endpoint-routing/

已经到中年,还坚持学习,欢迎大家交流批评指点
原文地址:https://www.cnblogs.com/Wadereye/p/12289237.html