IdentityServer里有各种Endpoint,如TokenEndpoint,UserInfoEndpoint,Authorize Endpoint,Discovery Endpoint等等。Endpoint从字面意思来看是“终端节点"或者“终节点”的意思。无独有偶NetCore的路由也有Endpoint的概念。那么我们提出一个问题来,究竟什么是Endpoint?
先来看一段代码,如下所示:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.ConfigureServices(svcs => svcs.AddRouting()).Configure( app => app.UseRouting().UseEndpoints( //test1 //endpoints => endpoints.MapGet("weather/{city}/{days}", //test2 endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast) )); }); private static Dictionary<string, string> _cities = new Dictionary<string, string>() { ["001"]="Beijing", ["028"]="Chengdu", ["091"]="Suzhou" }; //RequestDelegate public static async Task WeatherForecast(HttpContext context) { var city = (string)context.GetRouteData().Values["city"]; city = _cities[city]; //test1 //int days=int.Parse(context.GetRouteData().Values["days"].ToString()); //var report = new WeatherReport(city, days); //test2 int year = int.Parse(context.GetRouteData().Values["year"].ToString()); int month= int.Parse(context.GetRouteData().Values["month"].ToString()); int day=int.Parse(context.GetRouteData().Values["day"].ToString()); var report = new WeatherReport(city, new DateTime(year, month, day)); await RenderWeatherAsync(context, report); } private static async Task RenderWeatherAsync(HttpContext context,WeatherReport report) { context.Response.ContentType = "text/html;charset=utf-8"; await context.Response.WriteAsync("Weather "); await context.Response.WriteAsync($"{report.City}
"); foreach(var it in report.WeatherInfos) { await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}: "); await context.Response.WriteAsync($"{it.Value.LowTemperature}--{it.Value.HighTemperature}
"); } await context.Response.WriteAsync(""); } }
上面的代码很清晰,如果路由格式匹配”weather/{city}/{year}.{month}.{day}”,那么就调用WeatherForecast方法,这里的app.UseRouting().UseEndpoints就用到了Endpoint模式。要了解Endpoint,我们需要抽丝剥茧一步一步了解几个类的方法:
1.我们先看最里面的endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast),这里实际上调用的是EndpointRouteBuilderExtensions类的MapGet方法,代码如下所示:
////// Provides extension methods for to add endpoints. /// public static class EndpointRouteBuilderExtensions { // Avoid creating a new array every call private static readonly string[] GetVerb = new[] { "GET" }; private static readonly string[] PostVerb = new[] { "POST" }; private static readonly string[] PutVerb = new[] { "PUT" }; private static readonly string[] DeleteVerb = new[] { "DELETE" }; private static readonly string[] PatchVerb = new[] { "PATCH" }; /// /// Adds a to the that matches HTTP GET requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapGet( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, GetVerb, requestDelegate); } /// /// Adds a to the that matches HTTP POST requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapPost( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PostVerb, requestDelegate); } /// /// Adds a to the that matches HTTP PUT requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapPut( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PutVerb, requestDelegate); } /// /// Adds a to the that matches HTTP DELETE requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapDelete( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate); } /// /// Adds a to the that matches HTTP PATCH requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapPatch( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PatchVerb, requestDelegate); } /// /// Adds a to the that matches HTTP requests /// for the specified HTTP methods and pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// HTTP methods that the endpoint will match. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder MapMethods( this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, RequestDelegate requestDelegate) { if (httpMethods == null) { throw new ArgumentNullException(nameof(httpMethods)); } var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate); builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}"); builder.WithMetadata(new HttpMethodMetadata(httpMethods)); return builder; } /// /// Adds a to the that matches HTTP requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder Map( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate); } /// /// Adds a to the that matches HTTP requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } if (requestDelegate == null) { throw new ArgumentNullException(nameof(requestDelegate)); } const int defaultOrder = 0; //实例化RouteEndpointBuilder var builder = new RouteEndpointBuilder( requestDelegate, pattern, defaultOrder) { DisplayName = pattern.RawText ?? pattern.DebuggerToString(), }; // Add delegate attributes as metadata var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree if (attributes != null) { foreach (var attribute in attributes) { builder.Metadata.Add(attribute); } } //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests /// for the specified pattern. ///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备 var dataSource = endpoints.DataSources.OfType ().FirstOrDefault(); if (dataSource == null) { dataSource = new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); } return dataSource.AddEndpointBuilder(builder); } /// /// Adds a to the that matches HTTP GET requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapGet( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, GetVerb, handler); } /// /// Adds a to the that matches HTTP POST requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapPost( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PostVerb, handler); } /// /// Adds a to the that matches HTTP PUT requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapPut( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PutVerb, handler); } /// /// Adds a to the that matches HTTP DELETE requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapDelete( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, DeleteVerb, handler); } /// /// Adds a to the that matches HTTP PATCH requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapPatch( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PatchVerb, handler); } /// /// Adds a to the that matches HTTP requests /// for the specified HTTP methods and pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// HTTP methods that the endpoint will match. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder MapMethods( this IEndpointRouteBuilder endpoints, string pattern, IEnumerable<string> httpMethods, Delegate handler) { if (httpMethods is null) { throw new ArgumentNullException(nameof(httpMethods)); } var disableInferredBody = false; foreach (var method in httpMethods) { disableInferredBody = ShouldDisableInferredBody(method); if (disableInferredBody is true) { break; } } var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody); // Prepends the HTTP method to the DisplayName produced with pattern + method name builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}"); builder.WithMetadata(new HttpMethodMetadata(httpMethods)); return builder; static bool ShouldDisableInferredBody(string method) { // GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies return method.Equals(HttpMethods.Get, StringComparison.Ordinal) || method.Equals(HttpMethods.Delete, StringComparison.Ordinal) || method.Equals(HttpMethods.Head, StringComparison.Ordinal) || method.Equals(HttpMethods.Options, StringComparison.Ordinal) || method.Equals(HttpMethods.Trace, StringComparison.Ordinal) || method.Equals(HttpMethods.Connect, StringComparison.Ordinal); } } /// /// Adds a to the that matches HTTP requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return Map(endpoints, RoutePatternFactory.Parse(pattern), handler); } /// /// Adds a to the that matches HTTP requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, Delegate handler) { return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false); } /// /// Adds a specialized to the that will match /// requests for non-file-names with the lowest possible priority. /// /// The to add the route to. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. /// /// /// is intended to handle cases where URL path of /// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing /// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to /// result in an HTTP 404. /// /// /// registers an endpoint using the pattern /// {*path:nonfile}. The order of the registered endpoint will be int.MaxValue. /// /// public static RouteHandlerBuilder MapFallback(this IEndpointRouteBuilder endpoints, Delegate handler) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } return endpoints.MapFallback("{*path:nonfile}", handler); } /// /// Adds a specialized to the that will match /// the provided pattern with the lowest possible priority. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. /// /// /// is intended to handle cases where no /// other endpoint has matched. This is convenient for routing requests to a SPA framework. /// /// /// The order of the registered endpoint will be int.MaxValue. /// /// /// This overload will use the provided verbatim. Use the :nonfile route constraint /// to exclude requests for static files. /// /// public static RouteHandlerBuilder MapFallback( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } var conventionBuilder = endpoints.Map(pattern, handler); conventionBuilder.WithDisplayName("Fallback " + pattern); conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue); return conventionBuilder; } private static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, Delegate handler, bool disableInferBodyFromParameters) { if (endpoints is null) { throw new ArgumentNullException(nameof(endpoints)); } if (pattern is null) { throw new ArgumentNullException(nameof(pattern)); } if (handler is null) { throw new ArgumentNullException(nameof(handler)); } const int defaultOrder = 0; var routeParams = new List<string>(pattern.Parameters.Count); foreach (var part in pattern.Parameters) { routeParams.Add(part.Name); } var routeHandlerOptions = endpoints.ServiceProvider?.GetService >(); var options = new RequestDelegateFactoryOptions { ServiceProvider = endpoints.ServiceProvider, RouteParameterNames = routeParams, ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, DisableInferBodyFromParameters = disableInferBodyFromParameters, }; var requestDelegateResult = RequestDelegateFactory.Create(handler, options); var builder = new RouteEndpointBuilder( requestDelegateResult.RequestDelegate, pattern, defaultOrder) { DisplayName = pattern.RawText ?? pattern.DebuggerToString(), }; // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. builder.Metadata.Add(handler.Method); // Methods defined in a top-level program are generated as statics so the delegate // target will be null. Inline lambdas are compiler generated method so they can // be filtered that way. if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName) || !TypeHelper.IsCompilerGeneratedMethod(handler.Method)) { endpointName ??= handler.Method.Name; builder.DisplayName = $"{builder.DisplayName} => {endpointName}"; } // Add delegate attributes as metadata var attributes = handler.Method.GetCustomAttributes(); // Add add request delegate metadata foreach (var metadata in requestDelegateResult.EndpointMetadata) { builder.Metadata.Add(metadata); } // This can be null if the delegate is a dynamic method or compiled from an expression tree if (attributes is not null) { foreach (var attribute in attributes) { builder.Metadata.Add(attribute); } } var dataSource = endpoints.DataSources.OfType ().FirstOrDefault(); if (dataSource is null) { dataSource = new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); } return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder)); } } }
在这里我们重点看看下面签名的方法:
////// Adds a to the that matches HTTP requests /// for the specified pattern. /// /// The to add the route to. /// The route pattern. /// The delegate executed when the endpoint is matched. /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } if (requestDelegate == null) { throw new ArgumentNullException(nameof(requestDelegate)); } const int defaultOrder = 0; //实例化RouteEndpointBuilder var builder = new RouteEndpointBuilder( requestDelegate, pattern, defaultOrder) { DisplayName = pattern.RawText ?? pattern.DebuggerToString(), }; // Add delegate attributes as metadata var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree if (attributes != null) { foreach (var attribute in attributes) { builder.Metadata.Add(attribute); } } //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests /// for the specified pattern. ///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备 var dataSource = endpoints.DataSources.OfType ().FirstOrDefault(); if (dataSource == null) { dataSource = new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); } return dataSource.AddEndpointBuilder(builder); }
在这个方法里有一个实例化RouteEndpointBuilder的语句:
var builder = new RouteEndpointBuilder( requestDelegate, pattern, defaultOrder) { DisplayName = pattern.RawText ?? pattern.DebuggerToString(), };
其中RouteEndpointBuilder从字面意思看得出,就是RouteEndpoint的创建者,我们看下它的代码:
public sealed class RouteEndpointBuilder : EndpointBuilder { ////// Gets or sets the associated with this endpoint. /// public RoutePattern RoutePattern { get; set; } /// /// Gets or sets the order assigned to the endpoint. /// public int Order { get; set; } /// /// Constructs a new instance. /// /// The delegate used to process requests for the endpoint. /// The to use in URL matching. /// The order assigned to the endpoint. public RouteEndpointBuilder( RequestDelegate requestDelegate, RoutePattern routePattern, int order) { RequestDelegate = requestDelegate; RoutePattern = routePattern; Order = order; } //生成Route /// public override Endpoint Build() { if (RequestDelegate is null) { throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}."); } var routeEndpoint = new RouteEndpoint( RequestDelegate, RoutePattern, Order, new EndpointMetadataCollection(Metadata), DisplayName); return routeEndpoint; } }
再来看下Endpoint的代码:
////// Represents a logical endpoint in an application. /// public class Endpoint { /// /// Creates a new instance of . /// /// The delegate used to process requests for the endpoint. /// /// The endpoint . May be null. /// /// /// The informational display name of the endpoint. May be null. /// public Endpoint( RequestDelegate? requestDelegate, EndpointMetadataCollection? metadata, string? displayName) { // All are allowed to be null RequestDelegate = requestDelegate; Metadata = metadata ?? EndpointMetadataCollection.Empty; DisplayName = displayName; } /// /// Gets the informational display name of this endpoint. /// public string? DisplayName { get; } /// /// Gets the collection of metadata associated with this endpoint. /// public EndpointMetadataCollection Metadata { get; } /// /// Gets the delegate used to process requests for the endpoint. /// public RequestDelegate? RequestDelegate { get; } /// /// Returns a string representation of the endpoint. /// public override string? ToString() => DisplayName ?? base.ToString(); }
从Build()方法看得出,它利用路由匹配的RoutePattern,生成一个RouteEndpoint实例。RouteEndpoint叫做路由终点,继承自Endpoint。Endpoint有一个重要的RequestDelegate属性,用来处理当前的请求。看到这里,我们开始推断:所谓的Endpoint,无非就是用要匹配的pattern,构造一个RouteEndpoint的过程,其中RouteEndpoint继承自Endpoint。NetCore无非就是利用这个RouteEndpoint来匹配当前的Url,如果匹配得上,就执行RequestDelegate所代表的方法,上文就是WeatherForecast方法,如果匹配不上,则不执行WeatherForecast方法,仅此而已。
2.为了验证,我们再来看 app.UseRouting().UseEndpoints(),实际上调用的是EndpointRoutingApplicationBuilderExtensions类的两个方法:
public static class EndpointRoutingApplicationBuilderExtensions { private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; ////// Adds a middleware to the specified . /// /// The to add the middleware to. /// A reference to this instance after the operation has completed. /// /// /// A call to must be followed by a call to /// for the same /// instance. /// /// /// The defines a point in the middleware pipeline where routing decisions are /// made, and an is associated with the . The /// defines a point in the middleware pipeline where the current is executed. Middleware between /// the and may observe or change the /// associated with the . /// /// public static IApplicationBuilder UseRouting(this IApplicationBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } VerifyRoutingServicesAreRegistered(builder); IEndpointRouteBuilder endpointRouteBuilder; if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj)) { endpointRouteBuilder = (IEndpointRouteBuilder)obj!; // Let interested parties know if UseRouting() was called while a global route builder was set builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; } else { endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder); builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; } //先交由EndpointRoutingMiddleware中间件匹配Endpoint,然后交由下面的EndpointMiddleware处理 return builder.UseMiddleware (endpointRouteBuilder); } /// /// Adds a middleware to the specified /// with the instances built from configured . /// The will execute the associated with the current /// request. /// /// The to add the middleware to. /// An to configure the provided . /// A reference to this instance after the operation has completed. /// /// /// A call to must be preceded by a call to /// for the same /// instance. /// /// /// The defines a point in the middleware pipeline where routing decisions are /// made, and an is associated with the . The /// defines a point in the middleware pipeline where the current is executed. Middleware between /// the and may observe or change the /// associated with the . /// /// public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action 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); // Yes, this mutates an IOptions. We're registering data sources in a global collection which // can be used for discovery of endpoints or URL generation. // // Each middleware gets its own collection of data sources, and all of those data sources also // get added to a global collection. var routeOptions = builder.ApplicationServices.GetRequiredService >(); foreach (var dataSource in endpointRouteBuilder.DataSources) { if (!routeOptions.Value.EndpointDataSources.Contains(dataSource)) { routeOptions.Value.EndpointDataSources.Add(dataSource); } } //交由EndpointMiddleware处理 return builder.UseMiddleware (); } private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app) { // Verify if AddRouting was done before calling UseEndpointRouting/UseEndpoint // We use the RoutingMarkerService to make sure if all the services were added. if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) { throw new InvalidOperationException(Resources.FormatUnableToFindServices( nameof(IServiceCollection), nameof(RoutingServiceCollectionExtensions.AddRouting), "ConfigureServices(...)")); } } private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder) { if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj)) { var message = $"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " + $"execution pipeline before {nameof(EndpointMiddleware)}. " + $"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " + $"to 'Configure(...)' in the application startup code."; throw new InvalidOperationException(message); } endpointRouteBuilder = (IEndpointRouteBuilder)obj!; // This check handles the case where Map or something else that forks the pipeline is called between the two // routing middleware. if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder)) { var message = $"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " + $"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " + $"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline."; throw new InvalidOperationException(message); } } }
2-1首先看UseRouting()方法,这个方法其实就是调用EndpointRoutingMiddleware中间件进行Endpoint的匹配,我们可以看下它的几个方法:
internal sealed partial 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? _initializationTask; public EndpointRoutingMiddleware( MatcherFactory matcherFactory, ILogger 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) { // There's already an endpoint, skip matching completely var endpoint = httpContext.GetEndpoint(); if (endpoint != null) { Log.MatchSkipped(_logger, endpoint); return _next(httpContext); } // There's an inherent race condition between waiting for init and accessing the matcher // this is OK because once `_matcher` is initialized, it will not be set to null again. 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); // Awaited fallbacks for when the Tasks do not synchronously complete static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matcherTask) { var matcher = await matcherTask; //根据httpContext进行匹配 await matcher.MatchAsync(httpContext); await middleware.SetRoutingAndContinue(httpContext); } static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask) { await matchTask; await middleware.SetRoutingAndContinue(httpContext); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private Task SetRoutingAndContinue(HttpContext httpContext) { // If there was no mutation of the endpoint then log failure var endpoint = httpContext.GetEndpoint(); if (endpoint == null) { Log.MatchFailure(_logger); } else { // Raise an event if the route matched if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey)) { // We're just going to send the HttpContext since it has all of the relevant information _diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext); } Log.MatchSuccess(_logger, endpoint); } return _next(httpContext); } // Initialization is async to avoid blocking threads while reflection and things // of that nature take place. // // We've seen cases where startup is very slow if we allow multiple threads to race // while initializing the set of endpoints/routes. Doing CPU intensive work is a // blocking operation if you have a low core count and enough work to do. private Task InitializeAsync() { var initializationTask = _initializationTask; if (initializationTask != null) { return initializationTask; } return InitializeCoreAsync(); } private Task InitializeCoreAsync() { var initialization = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously); var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null); if (initializationTask != null) { // This thread lost the race, join the existing task. return initializationTask; } // This thread won the race, do the initialization. try { //用EndpointDataSource初始化matcher //EndpointDataSource来源于IEndpointRouteBuilder的DataSource属性 //见EndpointRouteBuilderExtensions类的201行 var matcher = _matcherFactory.CreateMatcher(_endpointDataSource); _initializationTask = Task.FromResult(matcher); // Complete the task, this will unblock any requests that came in while initializing. initialization.SetResult(matcher); return initialization.Task; } catch (Exception ex) { // Allow initialization to occur again. Since DataSources can change, it's possible // for the developer to correct the data causing the failure. _initializationTask = null; // Complete the task, this will throw for any requests that came in while initializing. initialization.SetException(ex); return initialization.Task; } } private static partial class Log { public static void MatchSuccess(ILogger logger, Endpoint endpoint) => MatchSuccess(logger, endpoint.DisplayName); [LoggerMessage(1, LogLevel.Debug, "Request matched endpoint '{EndpointName}'", EventName = "MatchSuccess")] private static partial void MatchSuccess(ILogger logger, string? endpointName); [LoggerMessage(2, LogLevel.Debug, "Request did not match any endpoints", EventName = "MatchFailure")] public static partial void MatchFailure(ILogger logger); public static void MatchSkipped(ILogger logger, Endpoint endpoint) => MatchingSkipped(logger, endpoint.DisplayName); [LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")] private static partial void MatchingSkipped(ILogger logger, string? endpointName); } }
从Invoke方法看得出来,它根据当前的HttpContext进行Endpoint的匹配,如果当前的HttpContext路由格式匹配成功,那么将当前HttpContext传递给下一个中间件处理,这个从SetRoutingAndContinue方法看得出来。
2-2其次看下UseEndpoints()方法,这个方法就是调用EndpointMiddleware中间件,对上面匹配成功的HttpContext进行处理,并调用HttpContext的EndPoint的RequestDelegate处理当前请求。我们重点看下它的Invoke方法,重点关注var requestTask = endpoint.RequestDelegate(httpContext):
public Task Invoke(HttpContext httpContext) { var endpoint = httpContext.GetEndpoint(); if (endpoint?.RequestDelegate != null) { if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata) { if (endpoint.Metadata.GetMetadata() != null && !httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey)) { ThrowMissingAuthMiddlewareException(endpoint); } if (endpoint.Metadata.GetMetadata () != null && !httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey)) { ThrowMissingCorsMiddlewareException(endpoint); } } Log.ExecutingEndpoint(_logger, endpoint); try { //调用HttpContext的Endpoint的RequestDelegate方法处理当前请求 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); } } }
总结:从上面的分析,我们粗略的了解了netcore路由的Endpoint模式其实就是一种用匹配模式构建的终端节点,它主要用来对HttpContext进行路由的匹配,如果匹配成功,则执行Endpoint上的RequestDelegate方法。