/// <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); // RequestDelegate can never be null on a RouteEndpoint. The nullability carries over from Endpoint. 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); } // 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); }
public void AddEndpoints( List <Endpoint> endpoints, HashSet <string> routeNames, ActionDescriptor action, IReadOnlyList <ConventionalRouteEntry> routes, IReadOnlyList <Action <EndpointBuilder> > groupConventions, IReadOnlyList <Action <EndpointBuilder> > conventions, bool createInertEndpoints, RoutePattern?groupPrefix = null) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (routeNames == null) { throw new ArgumentNullException(nameof(routeNames)); } if (action == null) { throw new ArgumentNullException(nameof(action)); } if (routes == null) { throw new ArgumentNullException(nameof(routes)); } if (conventions == null) { throw new ArgumentNullException(nameof(conventions)); } if (createInertEndpoints) { var builder = new InertEndpointBuilder() { DisplayName = action.DisplayName, RequestDelegate = _requestDelegate, }; AddActionDataToBuilder( builder, routeNames, action, routeName: null, dataTokens: null, suppressLinkGeneration: false, suppressPathMatching: false, groupConventions: groupConventions, conventions: conventions, perRouteConventions: Array.Empty <Action <EndpointBuilder> >()); endpoints.Add(builder.Build()); } if (action.AttributeRouteInfo?.Template == null) { // Check each of the conventional patterns to see if the action would be reachable. // If the action and pattern are compatible then create an endpoint with action // route values on the pattern. foreach (var route in routes) { // A route is applicable if: // 1. It has a parameter (or default value) for 'required' non-null route value // 2. It does not have a parameter (or default value) for 'required' null route value var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(route.Pattern, action.RouteValues); if (updatedRoutePattern == null) { continue; } updatedRoutePattern = RoutePatternFactory.Combine(groupPrefix, updatedRoutePattern); var requestDelegate = CreateRequestDelegate(action, route.DataTokens) ?? _requestDelegate; // We suppress link generation for each conventionally routed endpoint. We generate a single endpoint per-route // to handle link generation. var builder = new RouteEndpointBuilder(requestDelegate, updatedRoutePattern, route.Order) { DisplayName = action.DisplayName, }; AddActionDataToBuilder( builder, routeNames, action, route.RouteName, route.DataTokens, suppressLinkGeneration: true, suppressPathMatching: false, groupConventions: groupConventions, conventions: conventions, perRouteConventions: route.Conventions); endpoints.Add(builder.Build()); } } else { var requestDelegate = CreateRequestDelegate(action) ?? _requestDelegate; var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); // Modify the route and required values to ensure required values can be successfully subsituted. // Subsitituting required values into an attribute route pattern should always succeed. var(resolvedRoutePattern, resolvedRouteValues) = ResolveDefaultsAndRequiredValues(action, attributeRoutePattern); var updatedRoutePattern = _routePatternTransformer.SubstituteRequiredValues(resolvedRoutePattern, resolvedRouteValues); if (updatedRoutePattern == null) { // This kind of thing can happen when a route pattern uses a *reserved* route value such as `action`. // See: https://github.com/dotnet/aspnetcore/issues/14789 var formattedRouteKeys = string.Join(", ", resolvedRouteValues.Keys.Select(k => $"'{k}'")); throw new InvalidOperationException( $"Failed to update the route pattern '{resolvedRoutePattern.RawText}' with required route values. " + $"This can occur when the route pattern contains parameters with reserved names such as: {formattedRouteKeys} " + $"and also uses route constraints such as '{{action:int}}'. " + "To fix this error, choose a different parameter name."); } updatedRoutePattern = RoutePatternFactory.Combine(groupPrefix, updatedRoutePattern); var builder = new RouteEndpointBuilder(requestDelegate, updatedRoutePattern, action.AttributeRouteInfo.Order) { DisplayName = action.DisplayName, }; AddActionDataToBuilder( builder, routeNames, action, action.AttributeRouteInfo.Name, dataTokens: null, action.AttributeRouteInfo.SuppressLinkGeneration, action.AttributeRouteInfo.SuppressPathMatching, groupConventions: groupConventions, conventions: conventions, perRouteConventions: Array.Empty <Action <EndpointBuilder> >()); endpoints.Add(builder.Build()); } }
public void AddConventionalLinkGenerationRoute( List <Endpoint> endpoints, HashSet <string> routeNames, HashSet <string> keys, ConventionalRouteEntry route, IReadOnlyList <Action <EndpointBuilder> > groupConventions, IReadOnlyList <Action <EndpointBuilder> > conventions, RoutePattern?groupPrefix = null) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } if (keys == null) { throw new ArgumentNullException(nameof(keys)); } if (conventions == null) { throw new ArgumentNullException(nameof(conventions)); } var requiredValues = new RouteValueDictionary(); foreach (var key in keys) { if (route.Pattern.GetParameter(key) != null) { // Parameter (allow any) requiredValues[key] = RoutePattern.RequiredValueAny; } else if (route.Pattern.Defaults.TryGetValue(key, out var value)) { requiredValues[key] = value; } else { requiredValues[key] = null; } } // We have to do some massaging of the pattern to try and get the // required values to be correct. var pattern = _routePatternTransformer.SubstituteRequiredValues(route.Pattern, requiredValues); if (pattern == null) { // We don't expect this to happen, but we want to know if it does because it will help diagnose the bug. throw new InvalidOperationException("Failed to create a conventional route for pattern: " + route.Pattern); } pattern = RoutePatternFactory.Combine(groupPrefix, pattern); var builder = new RouteEndpointBuilder(context => Task.CompletedTask, pattern, route.Order) { DisplayName = "Route: " + route.Pattern.RawText, Metadata = { new SuppressMatchingMetadata(), }, }; if (route.RouteName != null) { builder.Metadata.Add(new RouteNameMetadata(route.RouteName)); } // See comments on the other usage of EndpointNameMetadata in this class. // // The set of cases for a conventional route are much simpler. We don't need to check // for Endpoint Name already exising here because there's no way to add an attribute to // a conventional route. if (route.RouteName != null && routeNames.Add(route.RouteName)) { builder.Metadata.Add(new EndpointNameMetadata(route.RouteName)); } for (var i = 0; i < groupConventions.Count; i++) { groupConventions[i](builder); } for (var i = 0; i < conventions.Count; i++) { conventions[i](builder); } for (var i = 0; i < route.Conventions.Count; i++) { route.Conventions[i](builder); } endpoints.Add((RouteEndpoint)builder.Build()); }
public void Combine_HandlesEmptyPatternsAndDuplicateSeperatorsInRawText(string leftTemplate, string rightTemplate) { var left = RoutePatternFactory.Parse(leftTemplate); var right = RoutePatternFactory.Parse(rightTemplate); var combined = RoutePatternFactory.Combine(left, right);