/// <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);
    }
Beispiel #2
0
    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());
        }
    }
Beispiel #3
0
    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);