.netcore入门16:aspnetcore之终结点路由工作原理

环境:

  • .netcore 3.1.1.0
  • vs2019 16.4.5

探索问题:

  1. http请求到来时,aspnetcore框架是怎样找到对应的action的?
  2. 路由模块Routing向http请求管道中注册了几个中间件?
  3. mvc模块有没有向http管道注册中间件?
  4. useRouting()UseEndpoints()这两个方法有什么区别,路由模块中为什么要分别注册这两个中间件?
  5. UseAuthorization()为什么要写在 useRouting()UseEndpoints()之间?

一、终结点EndPoint

EndPoint是路由匹配的目标,当http请求到来时,路由模块就会将请求匹配到某个EndPoint。EndPoint这个类封装了action的信息,比如:Controller类型、Action方法、[Attribute]情况等。在程序启动的时候,“mvc”模块就已经将所有的action转化成了Endpoint并交给“路由”模块去管理了,它的源码如下:

namespace Microsoft.AspNetCore.Http
{
    public class Endpoint
    {
        public Endpoint(
            RequestDelegate requestDelegate,
            EndpointMetadataCollection metadata,
            string displayName)
        {
            RequestDelegate = requestDelegate;
            Metadata = metadata ?? EndpointMetadataCollection.Empty;
            DisplayName = displayName;
        }        
        public string DisplayName { get; }        
        public EndpointMetadataCollection Metadata { get; }        
        public RequestDelegate RequestDelegate { get; }
        public override string ToString() => DisplayName ?? base.ToString();
    }
}

二、终结点工作流程概述

“路由”模块一前一后注册了两个中间件,第一个中间件(EndpointRoutingMiddleware)匹配到了EndPoint,第二个中间件(EndpointMiddleware)去调用已经匹配到了的EndPoint。“授权”模块会在这两个中间件之间注册一个中间件,所以授权模块在进行授权的时候可以明确当前的请求是指向哪个Action的。

三、整体代码执行流程(以webapi项目为例)

下面以webapi项目为例,讲解从项目的启动到http请求处理的核心环节,首先看下参照的startup.cs中简化的代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

这些代码的执行步骤如下:
程序启动阶段:

  • 第一步:执行services.AddControllers()
    将Controller的核心服务注册到容器中去
  • 第二步:执行app.UseRouting()
    将EndpointRoutingMiddleware中间件注册到http管道中
  • 第三步:执行app.UseAuthorization()
    将AuthorizationMiddleware中间件注册到http管道中
  • 第四步:执行app.UseEndpoints(encpoints=>endpoints.MapControllers())
    有两个主要的作用:
    1. 调用endpoints.MapControllers()将本程序集定义的所有Controller和Action转换为一个个的EndPoint并放到路由中间件的配置对象RouteOptions中
    2. 将EndpointMiddleware中间件注册到http管道中

当Http请求到来时

  • 第五步:收到一条http请求,此时EndpointRoutingMiddleware为它匹配一个Endpoint,并放到HttpContext中去
  • 第六步:授权中间件进行拦截,根据Endpoint的信息对这个请求进行授权验证。
  • 第七步:EndpointMiddleware中间件执行Endpoint中的RequestDelegate逻辑,即执行Controller的Action

四、aspnetcore各模块之间关系

上面几个步骤中主要涉及到“路由”、“授权”、“mvc”等几个模块。

  • “授权” 模块:Authorization
    “授权”模块注册了一个中间件进行授权拦截,拦截的对象是路由模块已匹配到的Endpoint。
  • “路由” 模块:Routing
    “路由”模块前后注册了两个中间件,第一个用来寻找匹配的终结点,并将这个终结点放到HttpContext中,这之后其他的中间件可以针对匹配的结果做一些事情(比如:授权中间件),第二个用来执行终结点,也就是调用Controller中的action方法。
  • “mvc” 模块:包括controllers、views和pages等小模块
    “mvc”模块并没有注册中间件,它的工作依赖于“路由”模块,在程序启动的时候将扫描所有的action并把他们封装成EndPoint交给“路由”模块,当http请求到来的时候就会被“路由”模块自动匹配和调用执行。

五、“路由”模块的两个中间件

说明:
在程序启动时每个action方法都会被封装成Endpoint对象交给路由模块,这样当http请求到来时,路由模块才会匹配到正确的Endpoint,进而找到Controller和Action!
“路由”模块注册中间件的源码如下(EndpointRoutingApplicationBuilderExtensions.cs):

namespace Microsoft.AspNetCore.Builder
{
    public static class EndpointRoutingApplicationBuilderExtensions
    {
        private const string EndpointRouteBuilder = "__EndpointRouteBuilder";
        public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            VerifyRoutingServicesAreRegistered(builder);

            var endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
            builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;

            return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
        }
        public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (configure == null)
            {
                throw new ArgumentNullException(nameof(configure));
            }

            VerifyRoutingServicesAreRegistered(builder);

            VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder);

            configure(endpointRouteBuilder);
            var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
            foreach (var dataSource in endpointRouteBuilder.DataSources)
            {
                routeOptions.Value.EndpointDataSources.Add(dataSource);
            }

            return builder.UseMiddleware<EndpointMiddleware>();
        }
        ......
    }
}

从上面的源码中可以看到:

  1. UseRouting()这个方法很简单就是注册了EndpointRoutingMiddleware中间件;
  2. UseEndpoints()这个方法是先生成一个EndpointRouteBuilder,然后调用我们的代码endpoints.MapControllers()将所有的action都包装成Endpoint放进了EndpointRouteBuilder的DataSources属性中,然后又将这些Endpoint放进了RouteOptionsEndpointDataSources属性中,最后注册了EndpointMiddleware中间件。

    至于action是怎么被封装成Endpoint的这里不做介绍(参考:.netcore入门13:aspnetcore源码之如何在程序启动时将Controller里的Action自动扫描封装成Endpoint),我们现在只要知道“路由”模块在程序启动的时候就已经将所有的action转化成了Endpoint以进行管理,并且注册了两个中间件。

5.1 EndpointRoutingMiddleware中间件:

这个中间件的主要作用是寻找匹配的Endpoint,我们直接看它的源码:

namespace Microsoft.AspNetCore.Routing
{
    internal sealed class EndpointRoutingMiddleware
    {
        private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched";

        private readonly MatcherFactory _matcherFactory;
        private readonly ILogger _logger;
        private readonly EndpointDataSource _endpointDataSource;
        private readonly DiagnosticListener _diagnosticListener;
        private readonly RequestDelegate _next;

        private Task<Matcher> _initializationTask;

        public EndpointRoutingMiddleware(
            MatcherFactory matcherFactory,
            ILogger<EndpointRoutingMiddleware> logger,
            IEndpointRouteBuilder endpointRouteBuilder,
            DiagnosticListener diagnosticListener,
            RequestDelegate next)
        {
            if (endpointRouteBuilder == null)
            {
                throw new ArgumentNullException(nameof(endpointRouteBuilder));
            }

            _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
            _next = next ?? throw new ArgumentNullException(nameof(next));

            _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
        }

        public Task Invoke(HttpContext httpContext)
        {
            var endpoint = httpContext.GetEndpoint();
            if (endpoint != null)
            {
                Log.MatchSkipped(_logger, endpoint);
                return _next(httpContext);
            }
            var matcherTask = InitializeAsync();
            if (!matcherTask.IsCompletedSuccessfully)
            {
                return AwaitMatcher(this, httpContext, matcherTask);
            }

            var matchTask = matcherTask.Result.MatchAsync(httpContext);
            if (!matchTask.IsCompletedSuccessfully)
            {
                return AwaitMatch(this, httpContext, matchTask);
            }
            return SetRoutingAndContinue(httpContext);
            
            static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task<Matcher> matcherTask)
            {
                var matcher = await matcherTask;
                await matcher.MatchAsync(httpContext);
                await middleware.SetRoutingAndContinue(httpContext);
            }

            static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
            {
                await matchTask;
                await middleware.SetRoutingAndContinue(httpContext);
            }
        }
        
        private Task SetRoutingAndContinue(HttpContext httpContext)
        {
            var endpoint = httpContext.GetEndpoint();
            if (endpoint == null)
            {
                Log.MatchFailure(_logger);
            }
            else
            {
                ......
                Log.MatchSuccess(_logger, endpoint);
            }
            return _next(httpContext);
        }
        
        private Task<Matcher> InitializeAsync()
        {
            ......
            return InitializeCoreAsync();
        }

        private Task<Matcher> InitializeCoreAsync()
        {
            var initialization = new TaskCompletionSource<Matcher>(TaskCreationOptions.RunContinuationsAsynchronously);
            ......
            try
            {
                var matcher = _matcherFactory.CreateMatcher(_endpointDataSource);
                ......
                initialization.SetResult(matcher);
                return initialization.Task;
            }
            catch (Exception ex)
            {
              ......
            }
        }
        ......
    }
}

我们从它的源码中可以看到,EndpointRoutingMiddleware中间件先是创建matcher,然后调用matcher.MatchAsync(httpContext)去匹配Endpoint(匹配到的结果自然就放在了HttpContext中),最后通过httpContext.GetEndpoint()验证了是否已经匹配到了正确的Endpoint并交给下个中间件继续执行!注意,在这里的代码中发现了方法中嵌套定义方法,这个是c#新的语法,可参考本地函数(C# 编程指南),不是语法错误。
下面来看看EndpointMiddleware中间件的源码:

5.2 EndpointMiddleware中间件:

namespace Microsoft.AspNetCore.Routing
{
    internal sealed class EndpointMiddleware
    {
        internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked";
        internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked";

        private readonly ILogger _logger;
        private readonly RequestDelegate _next;
        private readonly RouteOptions _routeOptions;

        public EndpointMiddleware(
            ILogger<EndpointMiddleware> logger,
            RequestDelegate next,
            IOptions<RouteOptions> routeOptions)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _routeOptions = routeOptions?.Value ?? throw new ArgumentNullException(nameof(routeOptions));
        }

        public Task Invoke(HttpContext httpContext)
        {
            var endpoint = httpContext.GetEndpoint();
            if (endpoint?.RequestDelegate != null)
            {
                if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
                {
                    if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
                        !httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
                    {
                        ThrowMissingAuthMiddlewareException(endpoint);
                    }

                    if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
                        !httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
                    {
                        ThrowMissingCorsMiddlewareException(endpoint);
                    }
                }

                Log.ExecutingEndpoint(_logger, endpoint);

                try
                {
                    var requestTask = endpoint.RequestDelegate(httpContext);
                    if (!requestTask.IsCompletedSuccessfully)
                    {
                        return AwaitRequestTask(endpoint, requestTask, _logger);
                    }
                }
                catch (Exception exception)
                {
                    Log.ExecutedEndpoint(_logger, endpoint);
                    return Task.FromException(exception);
                }

                Log.ExecutedEndpoint(_logger, endpoint);
                return Task.CompletedTask;
            }

            return _next(httpContext);

            static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
            {
                try
                {
                    await requestTask;
                }
                finally
                {
                    Log.ExecutedEndpoint(logger, endpoint);
                }
            }
        }
        ......
    }
}

从上面的模块中我们可以看到, EndpointMiddleware中间件先是从HttpContext中取出已经匹配了的Endpoint,如果Endpoint的RequestDelegate不为空的话就执行这个RequestDelegate,而这个RequestDelegate代表的是Controller的某个Action!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值