環境:
- .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())
有兩個主要的作用:- 調用
endpoints.MapControllers()
將本程序集定義的所有Controller和Action轉換爲一個個的EndPoint放到路由中間件的配置對象RouteOptions中 - 將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放進了RouteOptions
的EndpointDataSources
屬性中,最後註冊了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!