.netcore入門16:aspnetcore之終結點路由工作原理

環境:

  • .netcore 3.1.1.0
  • vs2019 16.4.5

試驗目的:
探索什麼是終結點路由,它和授權中間件、mvc是怎麼協作的?

結論:
先說下結論,“路由”模塊一前一後註冊了兩個中間件,第一個中間件匹配到了Controller的某個Action,第二個中間件去調用已經匹配到了的Action。“授權”模塊會在這兩個中間件之間註冊一箇中間件,所以授權模塊在進行授權的時候可以明確當前的請求是指向哪個Action的。

關於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();
    }
}

一、整體代碼執行流程

下面以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()
    將EndpointMiddleware中間件註冊到http管道中
  • 第三步:執行app.UseAuthorization()
    將Authorization中間件註冊到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並把他們封裝成action交給“路由”模塊,當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>();
        }
        ......
    }
}

從上面的源碼中可以看到:UseRouting()這個方法很簡單就是註冊了EndpointRoutingMiddleware中間件;UseEndpoints()這個方法是先生成一個EndpointRouteBuilder,然後調用我們的代碼endpoints.MapControllers()將所有的action都包裝成Endpoint放進了EndpointRouteBuilder的DataSources屬性中,然後又將這些Endpoint放進了RouteOptionsEndpointDataSources屬性中,最後註冊了EndpointMiddleware中間件。至於action是怎麼被封裝成Endpoint的這裏不做介紹(參考:.netcore入門13:aspnetcore源碼之如何在程序啓動時將Controller裏的Action自動掃描封裝成Endpoint),我們現在只關心“路由”模塊在程序啓動的時候就將所有的action轉化成了Endpoint以進行管理,並且註冊了兩個中間件。

  • 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.GetEndpoint()驗證了是否已經匹配到了正確的Endpoint並交個下箇中間件繼續執行!注意,在這裏的代碼中發現了方法中嵌套定義方法,這個是c#新的語法,可參考本地函數(C# 編程指南),不是語法錯誤。
下面來看看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!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章