private static (RoutePattern resolvedRoutePattern, IDictionary <string, string?> resolvedRequiredValues) ResolveDefaultsAndRequiredValues(ActionDescriptor action, RoutePattern attributeRoutePattern)
    {
        RouteValueDictionary?         updatedDefaults        = null;
        IDictionary <string, string?>?resolvedRequiredValues = null;

        foreach (var routeValue in action.RouteValues)
        {
            var parameter = attributeRoutePattern.GetParameter(routeValue.Key);

            if (!RouteValueEqualityComparer.Default.Equals(routeValue.Value, string.Empty))
            {
                if (parameter == null)
                {
                    // The attribute route has a required value with no matching parameter
                    // Add the required values without a parameter as a default
                    // e.g.
                    //   Template: "Login/{action}"
                    //   Required values: { controller = "Login", action = "Index" }
                    //   Updated defaults: { controller = "Login" }

                    if (updatedDefaults == null)
                    {
                        updatedDefaults = new RouteValueDictionary(attributeRoutePattern.Defaults);
                    }

                    updatedDefaults[routeValue.Key] = routeValue.Value;
                }
            }
            else
            {
                if (parameter != null)
                {
                    // The attribute route has a null or empty required value with a matching parameter
                    // Remove the required value from the route

                    if (resolvedRequiredValues == null)
                    {
                        resolvedRequiredValues = new Dictionary <string, string?>(action.RouteValues);
                    }

                    resolvedRequiredValues.Remove(parameter.Name);
                }
            }
        }
        if (updatedDefaults != null)
        {
            attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo !.Template !, updatedDefaults, parameterPolicies: null);
        }

        return(attributeRoutePattern, resolvedRequiredValues ?? action.RouteValues);
    }
Exemple #2
0
        private static void CopyNonParameterAmbientValues(
            RouteValueDictionary ambientValues,
            RouteValueDictionary acceptedValues,
            RouteValueDictionary combinedValues,
            RoutePattern _pattern)
        {
            if (ambientValues == null)
            {
                return;
            }

            foreach (var kvp in ambientValues)
            {
                if (IsRoutePartNonEmpty(kvp.Value))
                {
                    var parameter = _pattern.GetParameter(kvp.Key);
                    if (parameter == null && !acceptedValues.ContainsKey(kvp.Key))
                    {
                        combinedValues.Add(kvp.Key, kvp.Value);
                    }
                }
            }
        }
    public override TemplateBinder Create(RoutePattern pattern)
    {
        if (pattern == null)
        {
            throw new ArgumentNullException(nameof(pattern));
        }

        // Now create the constraints and parameter transformers from the pattern
        var policies = new List <(string parameterName, IParameterPolicy policy)>();

        foreach (var kvp in pattern.ParameterPolicies)
        {
            var parameterName = kvp.Key;

            // It's possible that we don't have an actual route parameter, we need to support that case.
            var parameter = pattern.GetParameter(parameterName);

            // Use the first parameter transformer per parameter
            var foundTransformer = false;
            for (var i = 0; i < kvp.Value.Count; i++)
            {
                var parameterPolicy = _policyFactory.Create(parameter, kvp.Value[i]);
                if (!foundTransformer && parameterPolicy is IOutboundParameterTransformer parameterTransformer)
                {
                    policies.Add((parameterName, parameterTransformer));
                    foundTransformer = true;
                }

                if (parameterPolicy is IRouteConstraint constraint)
                {
                    policies.Add((parameterName, constraint));
                }
            }
        }

        return(new TemplateBinder(UrlEncoder.Default, _pool, pattern, policies));
    }
    public override RoutePattern SubstituteRequiredValues(RoutePattern original, RouteValueDictionary requiredValues)
    {
        if (original is null)
        {
            throw new ArgumentNullException(nameof(original));
        }

        // Process each required value in sequence. Bail if we find any rejection criteria. The goal
        // of rejection is to avoid creating RoutePattern instances that can't *ever* match.
        //
        // If we succeed, then we need to create a new RoutePattern with the provided required values.
        //
        // Substitution can merge with existing RequiredValues already on the RoutePattern as long
        // as all of the success criteria are still met at the end.
        foreach (var kvp in requiredValues)
        {
            // There are three possible cases here:
            // 1. Required value is null-ish
            // 2. Required value is *any*
            // 3. Required value corresponds to a parameter
            // 4. Required value corresponds to a matching default value
            //
            // If none of these are true then we can reject this substitution.
            RoutePatternParameterPart parameter;
            if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty))
            {
                // 1. Required value is null-ish - check to make sure that this route doesn't have a
                // parameter or filter-like default.

                if (original.GetParameter(kvp.Key) != null)
                {
                    // Fail: we can't 'require' that a parameter be null. In theory this would be possible
                    // for an optional parameter, but that's not really in line with the usage of this feature
                    // so we don't handle it.
                    //
                    // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" }
                    return(null);
                }
                else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
                         !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
                {
                    // Fail: this route has a non-parameter default that doesn't match.
                    //
                    // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" }
                    return(null);
                }

                // Success: (for this parameter at least)
                //
                // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
                continue;
            }
            else if (RoutePattern.IsRequiredValueAny(kvp.Value))
            {
                // 2. Required value is *any* - this is allowed for a parameter with a default, but not
                // a non-parameter default.
                if (original.GetParameter(kvp.Key) == null &&
                    original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
                    !RouteValueEqualityComparer.Default.Equals(string.Empty, defaultValue))
                {
                    // Fail: this route as a non-parameter default that is stricter than *any*.
                    //
                    // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = *any* }
                    return(null);
                }

                // Success: (for this parameter at least)
                //
                // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = *any*, ... }
                continue;
            }
            else if ((parameter = original.GetParameter(kvp.Key)) != null)
            {
                // 3. Required value corresponds to a parameter - check to make sure that this value matches
                // any IRouteConstraint implementations.
                if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues))
                {
                    // Fail: this route has a constraint that failed.
                    //
                    // Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" }
                    return(null);
                }

                // Success: (for this parameter at least)
                //
                // Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... }
                continue;
            }
            else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
                     RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
            {
                // 4. Required value corresponds to a matching default value - check to make sure that this value matches
                // any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't
                // hurt for us to check.
                if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues))
                {
                    // Fail: this route has a constraint that failed.
                    //
                    // Ex:
                    //  Admin/Home/{action=Index}/{id?}
                    //  defaults: { area = "Admin" }
                    //  constraints: { area = "Blog" }
                    //  with required values: { area = "Admin" }
                    return(null);
                }

                // Success: (for this parameter at least)
                //
                // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... }
                continue;
            }
            else
            {
                // Fail: this is a required value for a key that doesn't appear in the templates, or the route
                // pattern has a different default value for a non-parameter.
                //
                // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... }
                // OR (less likely)
                // Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... }
                return(null);
            }
        }

        List <RoutePatternParameterPart> updatedParameters = null;
        List <RoutePatternPathSegment>   updatedSegments   = null;
        RouteValueDictionary             updatedDefaults   = null;

        // So if we get here, we're ready to update the route pattern. We need to update two things:
        // 1. Remove any default values that conflict with the required values.
        // 2. Merge any existing required values
        foreach (var kvp in requiredValues)
        {
            var parameter = original.GetParameter(kvp.Key);

            // We only need to handle the case where the required value maps to a parameter. That's the only
            // case where we allow a default and a required value to disagree, and we already validated the
            // other cases.
            //
            // If the required value is *any* then don't remove the default.
            if (parameter != null &&
                !RoutePattern.IsRequiredValueAny(kvp.Value) &&
                original.Defaults.TryGetValue(kvp.Key, out var defaultValue) &&
                !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue))
            {
                if (updatedDefaults == null && updatedSegments == null && updatedParameters == null)
                {
                    updatedDefaults   = new RouteValueDictionary(original.Defaults);
                    updatedSegments   = new List <RoutePatternPathSegment>(original.PathSegments);
                    updatedParameters = new List <RoutePatternParameterPart>(original.Parameters);
                }

                updatedDefaults.Remove(kvp.Key);
                RemoveParameterDefault(updatedSegments, updatedParameters, parameter);
            }
        }

        foreach (var kvp in original.RequiredValues)
        {
            requiredValues.TryAdd(kvp.Key, kvp.Value);
        }

        return(new RoutePattern(
                   original.RawText,
                   updatedDefaults ?? original.Defaults,
                   original.ParameterPolicies,
                   requiredValues,
                   updatedParameters ?? original.Parameters,
                   updatedSegments ?? original.PathSegments));
    }
Exemple #5
0
        public static TemplateValuesResult GetValues(
            KeyValuePair <string, object>[] _slots,
            KeyValuePair <string, object>[] _filters,
            string[] _requiredKeys,
            RoutePattern _pattern,
            RouteValueDictionary _defaults,
            RouteValueDictionary ambientValues,
            RouteValueDictionary values)
        {
            // Make a new copy of the slots array, we'll use this as 'scratch' space
            // and then the RVD will take ownership of it.
            var slots = new KeyValuePair <string, object> [_slots.Length];

            Array.Copy(_slots, 0, slots, 0, slots.Length);

            // Keeping track of the number of 'values' we've processed can be used to avoid doing
            // some expensive 'merge' operations later.
            var valueProcessedCount = 0;

            // Start by copying all of the values out of the 'values' and into the slots. There's no success
            // case where we *don't* use all of the 'values' so there's no reason not to do this up front
            // to avoid visiting the values dictionary again and again.
            for (var i = 0; i < slots.Length; i++)
            {
                var key = slots[i].Key;
                if (values.TryGetValue(key, out var value))
                {
                    // We will need to know later if the value in the 'values' was an null value.
                    // This affects how we process ambient values. Since the 'slots' are initialized
                    // with null values, we use the null-object-pattern to track 'explicit null', which means that
                    // null means omitted.
                    value    = IsRoutePartNonEmpty(value) ? value : SentinullValue.Instance;
                    slots[i] = new KeyValuePair <string, object>(key, value);

                    // Track the count of processed values - this allows a fast path later.
                    valueProcessedCount++;
                }
            }

            // In Endpoint Routing, patterns can have logical parameters that appear 'to the left' of
            // the route template. This governs whether or not the template can be selected (they act like
            // filters), and whether the remaining ambient values should be used.
            // should be used.
            // For example, in case of MVC it flattens out a route template like below
            //  {controller}/{action}/{id?}
            // to
            //  Products/Index/{id?},
            //  defaults: new { controller = "Products", action = "Index" },
            //  requiredValues: new { controller = "Products", action = "Index" }
            // In the above example, "controller" and "action" are no longer parameters.
            var copyAmbientValues = ambientValues != null;

            if (copyAmbientValues)
            {
                var requiredKeys = _requiredKeys;
                for (var i = 0; i < requiredKeys.Length; i++)
                {
                    // For each required key, the values and ambient values need to have the same value.
                    var key = requiredKeys[i];
                    var hasExplicitValue = values.TryGetValue(key, out var value);

                    if (ambientValues == null || !ambientValues.TryGetValue(key, out var ambientValue))
                    {
                        ambientValue = null;
                    }

                    // For now, only check ambient values with required values that don't have a parameter
                    // Ambient values for parameters are processed below
                    var hasParameter = _pattern.GetParameter(key) != null;
                    if (!hasParameter)
                    {
                        if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue))
                        {
                            throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern.");
                        }

                        if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]) &&
                            !ReferenceEquals(RoutePattern.RequiredValueAny, _pattern.RequiredValues[key]))
                        {
                            copyAmbientValues = false;
                            break;
                        }

                        if (hasExplicitValue && !RoutePartsEqual(value, ambientValue))
                        {
                            copyAmbientValues = false;
                            break;
                        }
                    }
                }
            }

            // We can now process the rest of the parameters (from left to right) and copy the ambient
            // values as long as the conditions are met.
            //
            // Find out which entries in the URI are valid for the URI we want to generate.
            // If the URI had ordered parameters a="1", b="2", c="3" and the new values
            // specified that b="9", then we need to invalidate everything after it. The new
            // values should then be a="1", b="9", c=<no value>.
            //
            // We also handle the case where a parameter is optional but has no value - we shouldn't
            // accept additional parameters that appear *after* that parameter.
            var parameters     = _pattern.Parameters;
            var parameterCount = _pattern.Parameters.Count;

            for (var i = 0; i < parameterCount; i++)
            {
                var key   = slots[i].Key;
                var value = slots[i].Value;

                // Whether or not the value was explicitly provided is signficant when comparing
                // ambient values. Remember that we're using a special sentinel value so that we
                // can tell the difference between an omitted value and an explicitly specified null.
                var hasExplicitValue = value != null;

                var hasAmbientValue = false;
                var ambientValue    = (object)null;

                var parameter = parameters[i];

                // We are copying **all** ambient values
                if (copyAmbientValues)
                {
                    hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue);
                    if (hasExplicitValue && hasAmbientValue && !RoutePartsEqual(ambientValue, value))
                    {
                        // Stop copying current values when we find one that doesn't match
                        copyAmbientValues = false;
                    }

                    if (!hasExplicitValue &&
                        !hasAmbientValue &&
                        _defaults?.ContainsKey(parameter.Name) != true)
                    {
                        // This is an unsatisfied parameter value and there are no defaults. We might still
                        // be able to generate a URL but we should stop 'accepting' ambient values.
                        //
                        // This might be a case like:
                        //  template: a/{b?}/{c?}
                        //  ambient: { c = 17 }
                        //  values: { }
                        //
                        // We can still generate a URL from this ("/a") but we shouldn't accept 'c' because
                        // we can't use it.
                        //
                        // In the example above we should fall into this block for 'b'.
                        copyAmbientValues = false;
                    }
                }

                // This might be an ambient value that matches a required value. We want to use these even if we're
                // not bulk-copying ambient values.
                //
                // This comes up in a case like the following:
                //  ambient-values: { page = "/DeleteUser", area = "Admin", }
                //  values: { controller = "Home", action = "Index", }
                //  pattern: {area}/{controller}/{action}/{id?}
                //  required-values: { area = "Admin", controller = "Home", action = "Index", page = (string)null, }
                //
                // OR in plain English... when linking from a page in an area to an action in the same area, it should
                // be possible to use the area as an ambient value.
                if (!copyAmbientValues && !hasExplicitValue && _pattern.RequiredValues.TryGetValue(key, out var requiredValue))
                {
                    hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue);
                    if (hasAmbientValue &&
                        (RoutePartsEqual(requiredValue, ambientValue) || ReferenceEquals(RoutePattern.RequiredValueAny, requiredValue)))
                    {
                        // Treat this an an explicit value to *force it*.
                        slots[i]         = new KeyValuePair <string, object>(key, ambientValue);
                        hasExplicitValue = true;
                        value            = ambientValue;
                    }
                }

                // If the parameter is a match, add it to the list of values we will use for URI generation
                if (hasExplicitValue && !ReferenceEquals(value, SentinullValue.Instance))
                {
                    // Already has a value in the list, do nothing
                }
                else if (copyAmbientValues && hasAmbientValue)
                {
                    slots[i] = new KeyValuePair <string, object>(key, ambientValue);
                }
                else if (parameter.IsOptional || parameter.IsCatchAll)
                {
                    // Value isn't needed for optional or catchall parameters - wipe out the key, so it
                    // will be omitted from the RVD.
                    slots[i] = default;
                }
                else if (_defaults != null && _defaults.TryGetValue(parameter.Name, out var defaultValue))
                {
                    // Add the default value only if there isn't already a new value for it and
                    // only if it actually has a default value.
                    slots[i] = new KeyValuePair <string, object>(key, defaultValue);
                }
                else
                {
                    // If we get here, this parameter needs a value, but doesn't have one. This is a
                    // failure case.
                    return(null);
                }
            }

            // Any default values that don't appear as parameters are treated like filters. Any new values
            // provided must match these defaults.
            var filters = _filters;

            for (var i = 0; i < filters.Length; i++)
            {
                var key   = filters[i].Key;
                var value = slots[i + parameterCount].Value;

                // We use a sentinel value here so we can track the different between omission and explicit null.
                // 'real null' means that the value was omitted.
                var hasExplictValue = value != null;
                if (hasExplictValue)
                {
                    // If there is a non-parameterized value in the route and there is a
                    // new value for it and it doesn't match, this route won't match.
                    if (!RoutePartsEqual(value, filters[i].Value))
                    {
                        return(null);
                    }
                }
                else
                {
                    // If no value was provided, then blank out this slot so that it doesn't show up in accepted values.
                    slots[i + parameterCount] = default;
                }
            }

            // At this point we've captured all of the 'known' route values, but we have't
            // handled an extra route values that were provided in 'values'. These all
            // need to be included in the accepted values.
            var acceptedValues = RouteValueDictionary.FromArray(slots);

            if (valueProcessedCount < values.Count)
            {
                // There are some values in 'value' that are unaccounted for, merge them into
                // the dictionary.
                foreach (var kvp in values)
                {
                    if (!_defaults.ContainsKey(kvp.Key))
                    {
#if RVD_TryAdd
                        acceptedValues.TryAdd(kvp.Key, kvp.Value);
#else
                        if (!acceptedValues.ContainsKey(kvp.Key))
                        {
                            acceptedValues.Add(kvp.Key, kvp.Value);
                        }
#endif
                    }
                }
            }

            // Currently this copy is required because BindValues will mutate the accepted values :(
            var combinedValues = new RouteValueDictionary(acceptedValues);

            // Add any ambient values that don't match parameters - they need to be visible to constraints
            // but they will ignored by link generation.
            CopyNonParameterAmbientValues(
                ambientValues: ambientValues,
                acceptedValues: acceptedValues,
                combinedValues: combinedValues, _pattern);

            return(new TemplateValuesResult()
            {
                AcceptedValues = acceptedValues,
                CombinedValues = combinedValues,
            });
        }
        // CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values
        // Because of default values it is possible for a route pattern to resolve to multiple endpoints
        private int CreateEndpoints(
            List <Endpoint> endpoints,
            ref StringBuilder patternStringBuilder,
            ActionDescriptor action,
            int routeOrder,
            RoutePattern routePattern,
            IReadOnlyDictionary <string, object> allDefaults,
            IReadOnlyDictionary <string, object> nonInlineDefaults,
            string name,
            RouteValueDictionary dataTokens,
            IDictionary <string, IList <IParameterPolicy> > allParameterPolicies,
            bool suppressLinkGeneration,
            bool suppressPathMatching)
        {
            var newPathSegments           = routePattern.PathSegments.ToList();
            var hasLinkGenerationEndpoint = false;

            // This is required because we create modified copies of the route pattern using its segments
            // A segment with a parameter will automatically include its policies
            // Non-parameter policies need to be manually included
            var nonParameterPolicyValues = routePattern.ParameterPolicies
                                           .Where(p => routePattern.GetParameter(p.Key ?? string.Empty) == null && p.Value.Count > 0 && p.Value.First().ParameterPolicy != null) // Only GetParameter is required. Extra is for safety
                                           .Select(p => new KeyValuePair <string, object>(p.Key, p.Value.First().ParameterPolicy))                                               // Can only pass a single non-parameter to RouteParameter
                                           .ToArray();
            var nonParameterPolicies = RouteValueDictionary.FromArray(nonParameterPolicyValues);

            // Create a mutable copy
            var nonInlineDefaultsCopy = nonInlineDefaults != null
                ? new RouteValueDictionary(nonInlineDefaults)
                : null;

            var resolvedRouteValues = ResolveActionRouteValues(action, allDefaults);

            for (var i = 0; i < newPathSegments.Count; i++)
            {
                // Check if the pattern can be shortened because the remaining parameters are optional
                //
                // e.g. Matching pattern {controller=Home}/{action=Index} against HomeController.Index
                // can resolve to the following endpoints: (sorted by RouteEndpoint.Order)
                // - /
                // - /Home
                // - /Home/Index
                if (UseDefaultValuePlusRemainingSegmentsOptional(
                        i,
                        action,
                        resolvedRouteValues,
                        allDefaults,
                        ref nonInlineDefaultsCopy,
                        newPathSegments))
                {
                    // The route pattern has matching default values AND an optional parameter
                    // For link generation we need to include an endpoint with parameters and default values
                    // so the link is correctly shortened
                    // e.g. {controller=Home}/{action=Index}/{id=17}
                    if (!hasLinkGenerationEndpoint)
                    {
                        var ep = CreateEndpoint(
                            action,
                            resolvedRouteValues,
                            name,
                            GetPattern(ref patternStringBuilder, newPathSegments),
                            nonParameterPolicies,
                            newPathSegments,
                            nonInlineDefaultsCopy,
                            routeOrder++,
                            dataTokens,
                            suppressLinkGeneration,
                            true);
                        endpoints.Add(ep);

                        hasLinkGenerationEndpoint = true;
                    }

                    var subPathSegments = newPathSegments.Take(i);

                    var subEndpoint = CreateEndpoint(
                        action,
                        resolvedRouteValues,
                        name,
                        GetPattern(ref patternStringBuilder, subPathSegments),
                        nonParameterPolicies,
                        subPathSegments,
                        nonInlineDefaultsCopy,
                        routeOrder++,
                        dataTokens,
                        suppressLinkGeneration,
                        suppressPathMatching);
                    endpoints.Add(subEndpoint);
                }

                UpdatePathSegments(i, action, resolvedRouteValues, routePattern, newPathSegments, ref allParameterPolicies);
            }

            var finalEndpoint = CreateEndpoint(
                action,
                resolvedRouteValues,
                name,
                GetPattern(ref patternStringBuilder, newPathSegments),
                nonParameterPolicies,
                newPathSegments,
                nonInlineDefaultsCopy,
                routeOrder++,
                dataTokens,
                suppressLinkGeneration,
                suppressPathMatching);

            endpoints.Add(finalEndpoint);

            return(routeOrder);

            string GetPattern(ref StringBuilder sb, IEnumerable <RoutePatternPathSegment> segments)
            {
                if (sb == null)
                {
                    sb = new StringBuilder();
                }

                RoutePatternWriter.WriteString(sb, segments);
                var rawPattern = sb.ToString();

                sb.Length = 0;

                return(rawPattern);
            }
        }