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("<html><head><title>Weather</title></head><body>"); await context.Response.WriteAsync($"<h3>{report.City}</h3>"); 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} <br/>"); } await context.Response.WriteAsync("</body></html>"); } }
上面的程式碼很清晰,如果路由格式匹配」weather/{city}/{year}.{month}.{day}」,那麼就呼叫WeatherForecast方法,這裡的app.UseRouting().UseEndpoints就用到了Endpoint模式。要了解Endpoint,我們需要抽絲剝繭一步一步瞭解幾個類的方法:
1.我們先看最裡面的endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast),這裡實際上呼叫的是EndpointRouteBuilderExtensions類的MapGet方法,程式碼如下所示:
/// <summary> /// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add endpoints. /// </summary> 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" }; /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder MapGet( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, GetVerb, requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder MapPost( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PostVerb, requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder MapPut( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PutVerb, requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder MapDelete( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder MapPatch( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return MapMethods(endpoints, pattern, PatchVerb, requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified HTTP methods and pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <param name="httpMethods">HTTP methods that the endpoint will match.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> 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; } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> public static IEndpointConventionBuilder Map( this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> 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<ModelEndpointDataSource>().FirstOrDefault(); if (dataSource == null) { dataSource = new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); } return dataSource.AddEndpointBuilder(builder); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder MapGet( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, GetVerb, handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder MapPost( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PostVerb, handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder MapPut( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PutVerb, handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder MapDelete( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, DeleteVerb, handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The <see cref="Delegate" /> executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder MapPatch( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return MapMethods(endpoints, pattern, PatchVerb, handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified HTTP methods and pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <param name="httpMethods">HTTP methods that the endpoint will match.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> 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); } } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return Map(endpoints, RoutePatternFactory.Parse(pattern), handler); } /// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> public static RouteHandlerBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, Delegate handler) { return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false); } /// <summary> /// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match /// requests for non-file-names with the lowest possible priority. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> /// <remarks> /// <para> /// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> 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. /// </para> /// <para> /// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> registers an endpoint using the pattern /// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>. /// </para> /// </remarks> 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); } /// <summary> /// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match /// the provided pattern with the lowest possible priority. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="handler">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> /// <remarks> /// <para> /// <see cref="MapFallback(IEndpointRouteBuilder, string, Delegate)"/> is intended to handle cases where no /// other endpoint has matched. This is convenient for routing requests to a SPA framework. /// </para> /// <para> /// The order of the registered endpoint will be <c>int.MaxValue</c>. /// </para> /// <para> /// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route constraint /// to exclude requests for static files. /// </para> /// </remarks> 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<IOptions<RouteHandlerOptions>>(); 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<ModelEndpointDataSource>().FirstOrDefault(); if (dataSource is null) { dataSource = new ModelEndpointDataSource(); endpoints.DataSources.Add(dataSource); } return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder)); } } }
在這裡我們重點看看下面簽名的方法:
/// <summary> /// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests /// for the specified pattern. /// </summary> /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param> /// <param name="pattern">The route pattern.</param> /// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param> /// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> 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<ModelEndpointDataSource>().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 { /// <summary> /// Gets or sets the <see cref="RoutePattern"/> associated with this endpoint. /// </summary> public RoutePattern RoutePattern { get; set; } /// <summary> /// Gets or sets the order assigned to the endpoint. /// </summary> public int Order { get; set; } /// <summary> /// Constructs a new <see cref="RouteEndpointBuilder"/> instance. /// </summary> /// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param> /// <param name="routePattern">The <see cref="RoutePattern"/> to use in URL matching.</param> /// <param name="order">The order assigned to the endpoint.</param> public RouteEndpointBuilder( RequestDelegate requestDelegate, RoutePattern routePattern, int order) { RequestDelegate = requestDelegate; RoutePattern = routePattern; Order = order; } //生成Route /// <inheritdoc /> 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的程式碼:
/// <summary> /// Represents a logical endpoint in an application. /// </summary> public class Endpoint { /// <summary> /// Creates a new instance of <see cref="Endpoint"/>. /// </summary> /// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param> /// <param name="metadata"> /// The endpoint <see cref="EndpointMetadataCollection"/>. May be null. /// </param> /// <param name="displayName"> /// The informational display name of the endpoint. May be null. /// </param> public Endpoint( RequestDelegate? requestDelegate, EndpointMetadataCollection? metadata, string? displayName) { // All are allowed to be null RequestDelegate = requestDelegate; Metadata = metadata ?? EndpointMetadataCollection.Empty; DisplayName = displayName; } /// <summary> /// Gets the informational display name of this endpoint. /// </summary> public string? DisplayName { get; } /// <summary> /// Gets the collection of metadata associated with this endpoint. /// </summary> public EndpointMetadataCollection Metadata { get; } /// <summary> /// Gets the delegate used to process requests for the endpoint. /// </summary> public RequestDelegate? RequestDelegate { get; } /// <summary> /// Returns a string representation of the endpoint. /// </summary> 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"; /// <summary> /// Adds a <see cref="EndpointRoutingMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>. /// </summary> /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param> /// <returns>A reference to this instance after the operation has completed.</returns> /// <remarks> /// <para> /// A call to <see cref="UseRouting(IApplicationBuilder)"/> must be followed by a call to /// <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> for the same <see cref="IApplicationBuilder"/> /// instance. /// </para> /// <para> /// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are /// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/> /// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between /// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the /// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>. /// </para> /// </remarks> 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<EndpointRoutingMiddleware>(endpointRouteBuilder); } /// <summary> /// Adds a <see cref="EndpointMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/> /// with the <see cref="EndpointDataSource"/> instances built from configured <see cref="IEndpointRouteBuilder"/>. /// The <see cref="EndpointMiddleware"/> will execute the <see cref="Endpoint"/> associated with the current /// request. /// </summary> /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param> /// <param name="configure">An <see cref="Action{IEndpointRouteBuilder}"/> to configure the provided <see cref="IEndpointRouteBuilder"/>.</param> /// <returns>A reference to this instance after the operation has completed.</returns> /// <remarks> /// <para> /// A call to <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> must be preceded by a call to /// <see cref="UseRouting(IApplicationBuilder)"/> for the same <see cref="IApplicationBuilder"/> /// instance. /// </para> /// <para> /// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are /// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/> /// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between /// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the /// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>. /// </para> /// </remarks> 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); // 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<IOptions<RouteOptions>>(); foreach (var dataSource in endpointRouteBuilder.DataSources) { if (!routeOptions.Value.EndpointDataSources.Contains(dataSource)) { routeOptions.Value.EndpointDataSources.Add(dataSource); } } //交由EndpointMiddleware處理 return builder.UseMiddleware<EndpointMiddleware>(); } 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<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) { // 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<Matcher> 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<Matcher> InitializeAsync() { var initializationTask = _initializationTask; if (initializationTask != null) { return initializationTask; } return InitializeCoreAsync(); } private Task<Matcher> InitializeCoreAsync() { var initialization = new TaskCompletionSource<Matcher>(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<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 { //呼叫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方法。