NetCore路由的Endpoint模式

2022-08-09 21:01:22

    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>");
        }
    }
View Code

 

 

上面的程式碼很清晰,如果路由格式匹配」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));
        }
    }
}
View Code

 

在這裡我們重點看看下面簽名的方法:

 

 /// <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);
        }
View Code

 

在這個方法裡有一個範例化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;
        }
    }
View Code

 

再來看下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();
    }
View Code

 

從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);
            }
        }
    }
View Code

   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);
        }
    }
View Code

從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);
                }
            }
        }
View Code

 

  總結:從上面的分析,我們粗略的瞭解了netcore路由的Endpoint模式其實就是一種用匹配模式構建的終端節點,它主要用來對HttpContext進行路由的匹配,如果匹配成功,則執行Endpoint上的RequestDelegate方法。