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