| File: EndpointDataSource.cs | Web Access |
| Project: src\aspnetcore\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Routing; /// <summary> /// Provides a collection of <see cref="Endpoint"/> instances. /// </summary> public abstract class EndpointDataSource { /// <summary> /// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Endpoint"/> /// instances. /// </summary> /// <returns>The <see cref="IChangeToken"/>.</returns> public abstract IChangeToken GetChangeToken(); /// <summary> /// Returns a read-only collection of <see cref="Endpoint"/> instances. /// </summary> public abstract IReadOnlyList<Endpoint> Endpoints { get; } /// <summary> /// Get the <see cref="Endpoint"/> instances for this <see cref="EndpointDataSource"/> given the specified <see cref="RouteGroupContext.Prefix"/> and <see cref="RouteGroupContext.Conventions"/>. /// </summary> /// <param name="context">Details about how the returned <see cref="Endpoint"/> instances should be grouped and a reference to application services.</param> /// <returns> /// Returns a read-only collection of <see cref="Endpoint"/> instances given the specified group <see cref="RouteGroupContext.Prefix"/> and <see cref="RouteGroupContext.Conventions"/>. /// </returns> public virtual IReadOnlyList<Endpoint> GetGroupedEndpoints(RouteGroupContext context) { // Only evaluate Endpoints once per call. var endpoints = Endpoints; var wrappedEndpoints = new RouteEndpoint[endpoints.Count]; for (int i = 0; i < endpoints.Count; i++) { var endpoint = endpoints[i]; // Endpoint does not provide a RoutePattern but RouteEndpoint does. So it's impossible to apply a prefix for custom Endpoints. // Supporting arbitrary Endpoints just to add group metadata would require changing the Endpoint type breaking any real scenario. if (endpoint is not RouteEndpoint routeEndpoint) { throw new NotSupportedException(Resources.FormatMapGroup_CustomEndpointUnsupported(endpoint.GetType())); } // Make the full route pattern visible to IEndpointConventionBuilder extension methods called on the group. // This includes patterns from any parent groups. var fullRoutePattern = RoutePatternFactory.Combine(context.Prefix, routeEndpoint.RoutePattern); var routeEndpointBuilder = new RouteEndpointBuilder(routeEndpoint.RequestDelegate, fullRoutePattern, routeEndpoint.Order) { DisplayName = routeEndpoint.DisplayName, ApplicationServices = context.ApplicationServices, }; // Apply group conventions to each endpoint in the group at a lower precedent than metadata already on the endpoint. foreach (var convention in context.Conventions) { convention(routeEndpointBuilder); } // Any metadata already on the RouteEndpoint must have been applied directly to the endpoint or to a nested group. // This makes the metadata more specific than what's being applied to this group. So add it after this group's conventions. foreach (var metadata in routeEndpoint.Metadata) { routeEndpointBuilder.Metadata.Add(metadata); } foreach (var finallyConvention in context.FinallyConventions) { finallyConvention(routeEndpointBuilder); } // The RoutePattern, RequestDelegate, Order and DisplayName can all be overridden by non-group-aware conventions. // Unlike with metadata, if a convention is applied to a group that changes any of these, I would expect these // to be overridden as there's no reasonable way to merge these properties. wrappedEndpoints[i] = (RouteEndpoint)routeEndpointBuilder.Build(); } return wrappedEndpoints; } // We don't implement DebuggerDisplay directly on the EndpointDataSource base type because this could have side effects. internal static string GetDebuggerDisplayStringForEndpoints(IReadOnlyList<Endpoint>? endpoints) { if (endpoints is null || endpoints.Count == 0) { return "No endpoints"; } var sb = new StringBuilder(); foreach (var endpoint in endpoints) { if (endpoint is RouteEndpoint routeEndpoint) { var template = routeEndpoint.RoutePattern.RawText; template = string.IsNullOrEmpty(template) ? "\"\"" : template; sb.Append(template); sb.Append(", Defaults: new { "); FormatValues(sb, routeEndpoint.RoutePattern.Defaults); sb.Append(" }"); var routeNameMetadata = routeEndpoint.Metadata.GetMetadata<IRouteNameMetadata>(); sb.Append(", Route Name: "); sb.Append(routeNameMetadata?.RouteName); var routeValues = routeEndpoint.RoutePattern.RequiredValues; if (routeValues.Count > 0) { sb.Append(", Required Values: new { "); FormatValues(sb, routeValues); sb.Append(" }"); } sb.Append(", Order: "); sb.Append(routeEndpoint.Order); var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>(); if (httpMethodMetadata is not null) { sb.Append(", Http Methods: "); sb.AppendJoin(", ", httpMethodMetadata.HttpMethods); } sb.Append(", Display Name: "); } else { sb.Append("Non-RouteEndpoint. DisplayName: "); } sb.AppendLine(endpoint.DisplayName); } return sb.ToString(); static void FormatValues(StringBuilder sb, IEnumerable<KeyValuePair<string, object?>> values) { var isFirst = true; foreach (var (key, value) in values) { if (isFirst) { isFirst = false; } else { sb.Append(", "); } sb.Append(key); sb.Append(" = "); if (value is null) { sb.Append("null"); } else { sb.Append('\"'); sb.Append(value); sb.Append('\"'); } } } } }