From 85c7bd8fac801ba8ad9a00418034ab86f5f21482 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Fri, 27 Jul 2018 05:09:14 -0700 Subject: [PATCH] Discard ambient values during link generation if the values do not match explicit values [Fixes #544] Link generation: Discard ambient values unless routing to the same address --- .../DefaultLinkGenerator.cs | 4 +- .../Template/TemplateBinder.cs | 381 +++++++++----- .../DefaultLinkGeneratorTest.cs | 491 ++++++++++++++---- .../EndpointFactory.cs | 35 ++ 4 files changed, 656 insertions(+), 255 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs diff --git a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs index 73ccbe092a..0d7df72432 100644 --- a/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs +++ b/src/Microsoft.AspNetCore.Routing/DefaultLinkGenerator.cs @@ -92,7 +92,9 @@ namespace Microsoft.AspNetCore.Routing var templateValuesResult = templateBinder.GetValues( ambientValues: context.AmbientValues, - values: context.ExplicitValues); + explicitValues: context.ExplicitValues, + endpoint.RequiredValues.Keys); + if (templateValuesResult == null) { // We're missing one of the required values for this route. diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index 241705523b..f4dacc09d0 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text.Encodings.Web; @@ -64,157 +65,71 @@ namespace Microsoft.AspNetCore.Routing.Template // Step 1: Get the list of values we're going to try to use to match and generate this URI public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values) + { + return GetValues(ambientValues: ambientValues, explicitValues: values, requiredKeys: null); + } + + internal TemplateValuesResult GetValues( + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + IEnumerable requiredKeys) { var context = new TemplateBindingContext(_defaults); - // 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=. - // - // 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. - for (var i = 0; i < _template.Parameters.Count; i++) + var canCopyParameterAmbientValues = true; + + // In case a route template is not parameterized, we do not know what the required parameters are, so look + // at required values of an endpoint to get the key names and then use them to decide if ambient values + // 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. + if (requiredKeys != null) { - var parameter = _template.Parameters[i]; + canCopyParameterAmbientValues = CanCopyParameterAmbientValues( + ambientValues: ambientValues, + explicitValues: explicitValues, + requiredKeys); + } - // If it's a parameter subsegment, examine the current value to see if it matches the new value - var parameterName = parameter.Name; - - object newParameterValue; - var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); - - object currentParameterValue = null; - var hasCurrentParameterValue = ambientValues != null && - ambientValues.TryGetValue(parameterName, out currentParameterValue); - - if (hasNewParameterValue && hasCurrentParameterValue) - { - if (!RoutePartsEqual(currentParameterValue, newParameterValue)) - { - // Stop copying current values when we find one that doesn't match - break; - } - } - - if (!hasNewParameterValue && - !hasCurrentParameterValue && - _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'. - break; - } - - // If the parameter is a match, add it to the list of values we will use for URI generation - if (hasNewParameterValue) - { - if (IsRoutePartNonEmpty(newParameterValue)) - { - context.Accept(parameterName, newParameterValue); - } - } - else - { - if (hasCurrentParameterValue) - { - context.Accept(parameterName, currentParameterValue); - } - } + if (canCopyParameterAmbientValues) + { + // Copy ambient values when no explicit values are provided + CopyParameterAmbientValues( + ambientValues: ambientValues, + explicitValues: explicitValues, + context); } // Add all remaining new values to the list of values we will use for URI generation - foreach (var kvp in values) - { - if (IsRoutePartNonEmpty(kvp.Value)) - { - context.Accept(kvp.Key, kvp.Value); - } - } + CopyNonParamaterExplicitValues(explicitValues, context); // Accept all remaining default values if they match a required parameter - for (var i = 0; i < _template.Parameters.Count; i++) + CopyParameterDefaultValues(context); + + if (!AllRequiredParametersHaveValue(context)) { - var parameter = _template.Parameters[i]; - if (parameter.IsOptional || parameter.IsCatchAll) - { - continue; - } - - if (context.NeedsValue(parameter.Name)) - { - // Add the default value only if there isn't already a new value for it and - // only if it actually has a default value, which we determine based on whether - // the parameter value is required. - context.AcceptDefault(parameter.Name); - } - } - - // Validate that all required parameters have a value. - for (var i = 0; i < _template.Parameters.Count; i++) - { - var parameter = _template.Parameters[i]; - if (parameter.IsOptional || parameter.IsCatchAll) - { - continue; - } - - if (!context.AcceptedValues.ContainsKey(parameter.Name)) - { - // We don't have a value for this parameter, so we can't generate a url. - return null; - } + return null; } // Any default values that don't appear as parameters are treated like filters. Any new values // provided must match these defaults. - foreach (var filter in _filters) + if (!FiltersMatch(explicitValues)) { - var parameter = GetParameter(filter.Key); - if (parameter != null) - { - continue; - } - - object value; - if (values.TryGetValue(filter.Key, out value)) - { - if (!RoutePartsEqual(value, filter.Value)) - { - // 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. - return null; - } - } + return null; } // Add any ambient values that don't match parameters - they need to be visible to constraints // but they will ignored by link generation. var combinedValues = new RouteValueDictionary(context.AcceptedValues); - if (ambientValues != null) - { - foreach (var kvp in ambientValues) - { - if (IsRoutePartNonEmpty(kvp.Value)) - { - var parameter = GetParameter(kvp.Key); - if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key)) - { - combinedValues.Add(kvp.Key, kvp.Value); - } - } - } - } + CopyNonParameterAmbientValues( + ambientValues: ambientValues, + combinedValues: combinedValues, + context); return new TemplateValuesResult() { @@ -255,21 +170,18 @@ namespace Microsoft.AspNetCore.Routing.Template else if (part.IsParameter) { // If it's a parameter, get its value - object value; - var hasValue = acceptedValues.TryGetValue(part.Name, out value); + var hasValue = acceptedValues.TryGetValue(part.Name, out var value); if (hasValue) { acceptedValues.Remove(part.Name); } var isSameAsDefault = false; - object defaultValue; - if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue)) + if (_defaults != null && + _defaults.TryGetValue(part.Name, out var defaultValue) && + RoutePartsEqual(value, defaultValue)) { - if (RoutePartsEqual(value, defaultValue)) - { - isSameAsDefault = true; - } + isSameAsDefault = true; } var converted = Convert.ToString(value, CultureInfo.InvariantCulture); @@ -414,6 +326,196 @@ namespace Microsoft.AspNetCore.Routing.Template } } + private bool CanCopyParameterAmbientValues( + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + IEnumerable requiredKeys) + { + foreach (var keyName in requiredKeys) + { + if (!explicitValues.TryGetValue(keyName, out var explicitValue)) + { + continue; + } + + object ambientValue = null; + var hasAmbientValue = ambientValues != null && + ambientValues.TryGetValue(keyName, out ambientValue); + + // This indicates an explicit value was provided whose key does not exist in ambient values + // Example: Link from controller-action to a page or vice versa. + if (!hasAmbientValue) + { + return false; + } + + if (!RoutePartsEqual(ambientValue, explicitValue)) + { + return false; + } + } + return true; + } + + private void CopyParameterAmbientValues( + RouteValueDictionary ambientValues, + RouteValueDictionary explicitValues, + TemplateBindingContext context) + { + // 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=. + // + // 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. + + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + + // If it's a parameter subsegment, examine the current value to see if it matches the new value + var parameterName = parameter.Name; + + var hasExplicitValue = explicitValues.TryGetValue(parameterName, out var explicitValue); + + object ambientValue = null; + var hasAmbientValue = ambientValues != null && + ambientValues.TryGetValue(parameterName, out ambientValue); + + if (hasExplicitValue && hasAmbientValue && !RoutePartsEqual(ambientValue, explicitValue)) + { + // Stop copying current values when we find one that doesn't match + break; + } + + 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'. + break; + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasExplicitValue) + { + if (IsRoutePartNonEmpty(explicitValue)) + { + context.Accept(parameterName, explicitValue); + } + } + else if (hasAmbientValue) + { + context.Accept(parameterName, ambientValue); + } + } + } + + private void CopyNonParamaterExplicitValues(RouteValueDictionary explicitValues, TemplateBindingContext context) + { + foreach (var kvp in explicitValues) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + context.Accept(kvp.Key, kvp.Value); + } + } + } + + private void CopyParameterDefaultValues(TemplateBindingContext context) + { + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (context.NeedsValue(parameter.Name)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + context.AcceptDefault(parameter.Name); + } + } + } + + private bool AllRequiredParametersHaveValue(TemplateBindingContext context) + { + for (var i = 0; i < _template.Parameters.Count; i++) + { + var parameter = _template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (!context.AcceptedValues.ContainsKey(parameter.Name)) + { + // We don't have a value for this parameter, so we can't generate a url. + return false; + } + } + return true; + } + + private bool FiltersMatch(RouteValueDictionary explicitValues) + { + foreach (var filter in _filters) + { + var parameter = GetParameter(filter.Key); + if (parameter != null) + { + continue; + } + + if (explicitValues.TryGetValue(filter.Key, out var value) && !RoutePartsEqual(value, filter.Value)) + { + // 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. + return false; + } + } + return true; + } + + private void CopyNonParameterAmbientValues( + RouteValueDictionary ambientValues, + RouteValueDictionary combinedValues, + TemplateBindingContext context) + { + if (ambientValues == null) + { + return; + } + + foreach (var kvp in ambientValues) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + var parameter = GetParameter(kvp.Key); + if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key)) + { + combinedValues.Add(kvp.Key, kvp.Value); + } + } + } + } + [DebuggerDisplay("{DebuggerToString(),nq}")] private struct TemplateBindingContext { @@ -444,8 +546,7 @@ namespace Microsoft.AspNetCore.Routing.Template { Debug.Assert(!_acceptedValues.ContainsKey(key)); - object value; - if (_defaults != null && _defaults.TryGetValue(key, out value)) + if (_defaults != null && _defaults.TryGetValue(key, out var value)) { _acceptedValues.Add(key, value); } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 0321c730f2..d74d2eff48 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.EndpointFinders; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Matchers; -using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; @@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_Success() { // Arrange - var endpoint = CreateEndpoint("{controller}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext(new { controller = "Home" }); @@ -47,7 +46,7 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var expectedMessage = "Could not find a matching endpoint to generate a link."; - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext(new { controller = "Home" }); @@ -67,7 +66,7 @@ namespace Microsoft.AspNetCore.Routing public void TryGetLink_Fail() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext(new { controller = "Home" }); @@ -90,9 +89,9 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_MultipleEndpoints_Success() { // Arrange - var endpoint1 = CreateEndpoint("{controller}/{action}/{id?}"); - var endpoint2 = CreateEndpoint("{controller}/{action}"); - var endpoint3 = CreateEndpoint("{controller}"); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id?}"); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); + var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext(new { controller = "Home", action = "Index", id = "10" }); @@ -113,9 +112,9 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_MultipleEndpoints_Success2() { // Arrange - var endpoint1 = CreateEndpoint("{controller}/{action}/{id}"); - var endpoint2 = CreateEndpoint("{controller}/{action}"); - var endpoint3 = CreateEndpoint("{controller}"); + var endpoint1 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{id}"); + var endpoint2 = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); + var endpoint3 = EndpointFactory.CreateMatcherEndpoint("{controller}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext(new { controller = "Home", action = "Index" }); @@ -136,10 +135,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_EncodesValues() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { name = "name with %special #characters" }, + explicitValues: new { name = "name with %special #characters" }, ambientValues: new { controller = "Home", action = "Index" }); // Act @@ -159,7 +158,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_ForListOfStrings() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( new { color = new List { "red", "green", "blue" } }, @@ -182,7 +181,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_ForListOfInts() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( new { items = new List { 10, 20, 30 } }, @@ -205,7 +204,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_ForList_Empty() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( new { color = new List { } }, @@ -228,7 +227,7 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_ForList_StringWorkaround() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, @@ -251,10 +250,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_Success_AmbientValues() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index" }, + explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act @@ -274,10 +273,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_GeneratesLowercaseUrl_SetOnRouteOptions() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index" }, + explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act @@ -297,11 +296,11 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_GeneratesLowercaseQueryString_SetOnRouteOptions() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act @@ -321,11 +320,11 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_GeneratesLowercaseQueryString_OnlyIfLowercaseUrlIsTrue_SetOnRouteOptions() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act @@ -345,10 +344,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_AppendsTrailingSlash_SetOnRouteOptions() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index" }, + explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act @@ -368,11 +367,11 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true, AppendTrailingSlash = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act @@ -392,10 +391,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "InDex" }, + explicitValues: new { action = "InDex" }, ambientValues: new { controller = "HoMe" }); // Act @@ -416,10 +415,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { LowercaseUrls = false }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "InDex" }, + explicitValues: new { action = "InDex" }, ambientValues: new { controller = "HoMe" }); // Act @@ -440,11 +439,11 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = true, LowercaseQueryStrings = true }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act @@ -466,11 +465,11 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator( new RouteOptions() { LowercaseUrls = false, LowercaseQueryStrings = false }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, + explicitValues: new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }, ambientValues: new { controller = "Home" }); // Act @@ -492,10 +491,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}"); var linkGenerator = CreateLinkGenerator(new RouteOptions() { AppendTrailingSlash = false }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index" }, + explicitValues: new { action = "Index" }, ambientValues: new { controller = "Home" }); // Act @@ -519,9 +518,9 @@ namespace Microsoft.AspNetCore.Routing var context = CreateRouteValuesContext(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", - new { p2 = "catchall" }, + defaults: new { p2 = "catchall" }, constraints: new { p2 = "\\d{4}" }); // Act @@ -546,10 +545,10 @@ namespace Microsoft.AspNetCore.Routing var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", - new { p2 = "catchall" }, - new { p2 = new RegexRouteConstraint("\\d{4}"), }); + defaults: new { p2 = "catchall" }, + constraints: new { p2 = new RegexRouteConstraint("\\d{4}"), }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -574,10 +573,10 @@ namespace Microsoft.AspNetCore.Routing var context = CreateRouteValuesContext(new { p1 = "abcd" }); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", - new { p2 = "catchall" }, - new { p2 = new RegexRouteConstraint("\\d{4}") }); + defaults: new { p2 = "catchall" }, + constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -601,10 +600,10 @@ namespace Microsoft.AspNetCore.Routing var context = CreateRouteValuesContext(new { p1 = "hello", p2 = "1234" }); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{*p2}", - new { p2 = "catchall" }, - new { p2 = new RegexRouteConstraint("\\d{4}") }); + defaults: new { p2 = "catchall" }, + constraints: new { p2 = new RegexRouteConstraint("\\d{4}") }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -640,7 +639,7 @@ namespace Microsoft.AspNetCore.Routing .Returns(true) .Verifiable(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( "{p1}/{p2}", defaults: new { p2 = "catchall" }, constraints: new { p2 = target.Object }); @@ -670,13 +669,13 @@ namespace Microsoft.AspNetCore.Routing // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store" }, constraints: new { c = constraint }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Store" }, + explicitValues: new { action = "Store" }, ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); var expectedValues = new RouteValueDictionary( @@ -707,13 +706,13 @@ namespace Microsoft.AspNetCore.Routing // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17" }, constraints: new { c = constraint }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Store" }, + explicitValues: new { action = "Store" }, ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( @@ -741,13 +740,13 @@ namespace Microsoft.AspNetCore.Routing // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/{controller}/{action}", defaults: new { action = "Index" }, constraints: new { c = constraint, }); var context = CreateRouteValuesContext( - suppliedValues: new { controller = "Shopping" }, + explicitValues: new { controller = "Shopping" }, ambientValues: new { controller = "Home", action = "Blog" }); var expectedValues = new RouteValueDictionary( @@ -776,13 +775,13 @@ namespace Microsoft.AspNetCore.Routing // Arrange var constraint = new CapturingConstraint(); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "slug/Home/Store", defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, constraints: new { c = constraint, }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Store", thirdthing = "13" }, + explicitValues: new { action = "Store", thirdthing = "13" }, ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); var expectedValues = new RouteValueDictionary( @@ -808,13 +807,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = 4 }); + explicitValues: new { action = "Index", controller = "Home", id = 4 }); // Act var link = linkGenerator.GetLink( @@ -835,13 +834,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); + explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -863,12 +862,12 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = 98 }); + explicitValues: new { action = "Index", controller = "Home", id = 98 }); // Act var link = linkGenerator.GetLink( @@ -889,13 +888,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); + explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( @@ -916,13 +915,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index" }, constraints: new { id = "int" }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); + explicitValues: new { action = "Index", controller = "Home", id = "not-an-integer" }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -944,13 +943,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = 14 }); + explicitValues: new { action = "Index", controller = "Home", id = 14 }); // Act var link = linkGenerator.GetLink( @@ -971,13 +970,13 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{id:int:range(1,20)}", defaults: new { controller = "Home", action = "Index" }, constraints: new { }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", id = 50 }); + explicitValues: new { action = "Index", controller = "Home", id = 50 }); // Act var canGenerateLink = linkGenerator.TryGetLink( @@ -1000,13 +999,13 @@ namespace Microsoft.AspNetCore.Routing // Arrange var constraint = new MaxLengthRouteConstraint(20); var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "Home/Index/{name}", defaults: new { controller = "Home", action = "Index" }, constraints: new { name = constraint }); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products" }); + explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( @@ -1026,10 +1025,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_ParameterPresentInValues() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}/{name?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products" }); + explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( @@ -1048,10 +1047,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_ParameterNotPresentInValues() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}/{name?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); + explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( @@ -1070,12 +1069,12 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_ParameterPresentInValuesAndDefaults() { // Arrange - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "default-products" }); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products" }); + explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( @@ -1094,12 +1093,12 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() { // Arrange - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/{name}", defaults: new { name = "products" }); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); + explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( @@ -1118,10 +1117,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_ParameterNotPresentInTemplate_PresentInValues() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}/{name}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products", format = "json" }); + explicitValues: new { action = "Index", controller = "Home", name = "products", format = "json" }); // Act var link = linkGenerator.GetLink( @@ -1141,11 +1140,11 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint( + var endpoint = EndpointFactory.CreateMatcherEndpoint( template: "{controller}/{action}/.{name?}"); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home", name = "products" }); + explicitValues: new { action = "Index", controller = "Home", name = "products" }); // Act var link = linkGenerator.GetLink( @@ -1166,10 +1165,10 @@ namespace Microsoft.AspNetCore.Routing { // Arrange var linkGenerator = CreateLinkGenerator(); - var endpoint = CreateEndpoint("{controller}/{action}/.{name?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/.{name?}"); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); + explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( @@ -1189,10 +1188,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameter_InSimpleSegment() { // Arrange - var endpoint = CreateEndpoint("{controller}/{action}/{name?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{name?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { action = "Index", controller = "Home" }); + explicitValues: new { action = "Index", controller = "Home" }); // Act var link = linkGenerator.GetLink( @@ -1211,10 +1210,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_TwoOptionalParameters_OneValueFromAmbientValues() { // Arrange - var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { }, + explicitValues: new { }, ambientValues: new { c = "17" }); // Act @@ -1234,10 +1233,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_OptionalParameterAfterDefault_OneValueFromAmbientValues() { // Arrange - var endpoint = CreateEndpoint("a/{b=15}/{c?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { }, + explicitValues: new { }, ambientValues: new { c = "17" }); // Act @@ -1257,10 +1256,10 @@ namespace Microsoft.AspNetCore.Routing public void GetLink_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() { // Arrange - var endpoint = CreateEndpoint("a/{b=15}/{c?}/{d?}"); + var endpoint = EndpointFactory.CreateMatcherEndpoint("a/{b=15}/{c?}/{d?}"); var linkGenerator = CreateLinkGenerator(); var context = CreateRouteValuesContext( - suppliedValues: new { }, + explicitValues: new { }, ambientValues: new { d = "17" }); // Act @@ -1276,28 +1275,292 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/a", link); } - private RouteValuesBasedEndpointFinderContext CreateRouteValuesContext(object suppliedValues, object ambientValues = null) + public static TheoryData DoesNotDiscardAmbientValuesData { - var context = new RouteValuesBasedEndpointFinderContext(); - context.ExplicitValues = new RouteValueDictionary(suppliedValues); - context.AmbientValues = new RouteValueDictionary(ambientValues); - return context; + get + { + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData + { + // link to same action on same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action on same controller - ignoring case + { + new { controller = "ProDUcts", action = "EDit", id = 10 }, + new { controller = "ProDUcts", action = "EDit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller on same area + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "Admin", controller = "Products", action = "Edit" }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller on same area + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Admin", controller = "Products", action = "Edit", page = (string)null } + }, + + // link to same action and same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = "", controller = "Products", action = "Edit", page = "" }, + new { area = "", controller = "Products", action = "Edit", page = "" } + }, + + // link to same page + { + new { page = "Products/Edit", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } + }, + }; + } } - private MatcherEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - int order = 0, - EndpointMetadataCollection metadata = null) + [Theory] + [MemberData(nameof(DoesNotDiscardAmbientValuesData))] + public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) { - return new MatcherEndpoint( - MatcherEndpoint.EmptyInvoker, - RoutePatternFactory.Parse(template, defaults, constraints), - new RouteValueDictionary(), - order, - metadata, - null); + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + explicitValues: explicitValues, + ambientValues: ambientValues); + + // Act + var canGenerateLink = linkGenerator.TryGetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, + out var link); + + // Assert + Assert.True(canGenerateLink); + Assert.Equal("/Products/Edit/10", link); + } + + [Fact] + public void TryGetLink_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + explicitValues: new { c = "Products", a = "Edit" }, + ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var canGenerateLink = linkGenerator.TryGetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, + out var link); + + // Assert + Assert.True(canGenerateLink); + Assert.Equal("/Products/Edit/10", link); + } + + [Fact] + public void TryGetLink_DiscardsAmbientValues_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + explicitValues: new { c = "Products", a = "List" }, + ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var canGenerateLink = linkGenerator.TryGetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, + out var link); + + // Assert + Assert.False(canGenerateLink); + Assert.Null(link); + } + + public static TheoryData DiscardAmbientValuesData + { + get + { + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData + { + // link to different action on same controller + { + new { controller = "Products", action = "Edit", id = 10 }, + new { controller = "Products", action = "List" }, + new { area = (string)null, controller = "Products", action = "List", page = (string)null }, + new { area = (string)null, controller = "Products", action = "List", page = (string)null } + }, + + // link to different action on same controller and same area + { + new { area = "Customer", controller = "Products", action = "Edit", id = 10 }, + new { area = "Customer", controller = "Products", action = "List" }, + new { area = "Customer", controller = "Products", action = "List", page = (string)null }, + new { area = "Customer", controller = "Products", action = "List", page = (string)null } + }, + + // link from one area to a different one + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "Consumer", controller = "Products", action = "Edit" }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from non-area to a area one + { + new { controller = "Products", action = "Edit", id = 10 }, + new { area = "Consumer", controller = "Products", action = "Edit" }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null }, + new { area = "Consumer", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from area to a non-area based action + { + new { area = "Admin", controller = "Products", action = "Edit", id = 10 }, + new { area = "", controller = "Products", action = "Edit" }, + new { area = "", controller = "Products", action = "Edit", page = (string)null }, + new { area = "", controller = "Products", action = "Edit", page = (string)null } + }, + + // link from controller-action to a page + { + new { controller = "Products", action = "Edit", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"}, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit"} + }, + + // link from a page to controller-action + { + new { page = "Products/Edit", id = 10 }, + new { controller = "Products", action = "Edit" }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null }, + new { area = (string)null, controller = "Products", action = "Edit", page = (string)null } + }, + + // link from one page to a different page + { + new { page = "Products/Details", id = 10 }, + new { page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" }, + new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } + }, + }; + } + } + + [Theory] + [MemberData(nameof(DiscardAmbientValuesData))] + public void TryGetLink_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) + { + // Linking to a different action on the same controller + + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(); + var context = CreateRouteValuesContext( + explicitValues: explicitValues, + ambientValues: ambientValues); + + // Act + var canGenerateLink = linkGenerator.TryGetLink( + new LinkGeneratorContext + { + Endpoints = new[] { endpoint }, + ExplicitValues = context.ExplicitValues, + AmbientValues = context.AmbientValues + }, + out var link); + + // Assert + Assert.False(canGenerateLink); + Assert.Null(link); + } + + private RouteValuesBasedEndpointFinderContext CreateRouteValuesContext( + object explicitValues, + object ambientValues = null) + { + var context = new RouteValuesBasedEndpointFinderContext(); + context.ExplicitValues = new RouteValueDictionary(explicitValues); + context.AmbientValues = new RouteValueDictionary(ambientValues); + return context; } private LinkGenerator CreateLinkGenerator(RouteOptions routeOptions = null) diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs new file mode 100644 index 0000000000..6286e2781f --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Routing +{ + internal static class EndpointFactory + { + public static MatcherEndpoint CreateMatcherEndpoint( + string template, + object defaults = null, + object constraints = null, + object requiredValues = null, + int order = 0, + string displayName = null, + params object[] metadata) + { + var metadataCollection = EndpointMetadataCollection.Empty; + if (metadata != null) + { + metadataCollection = new EndpointMetadataCollection(metadata); + } + + return new MatcherEndpoint( + MatcherEndpoint.EmptyInvoker, + RoutePatternFactory.Parse(template, defaults, constraints), + new RouteValueDictionary(requiredValues), + order, + metadataCollection, + displayName); + } + } +}