diff --git a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj index 386d4eb8cb..8b6727ba9a 100644 --- a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj +++ b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj @@ -70,6 +70,7 @@ + diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs index d4e4dc78bf..877286d874 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Routing.Template } // Step 1: Get the list of values we're going to try to use to match and generate this URI - public IDictionary GetAcceptedValues(IDictionary ambientValues, + public TemplateValuesResult GetValues(IDictionary ambientValues, IDictionary values) { var context = new TemplateBindingContext(_defaults, values); @@ -145,7 +145,29 @@ namespace Microsoft.AspNet.Routing.Template } } - return context.AcceptedValues; + // 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 Dictionary(context.AcceptedValues, StringComparer.OrdinalIgnoreCase); + 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); + } + } + } + } + + return new TemplateValuesResult() + { + AcceptedValues = context.AcceptedValues, + CombinedValues = combinedValues, + }; } // Step 2: If the route is a match generate the appropriate URI diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 4af40c6e2c..382fc01d57 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -103,7 +103,7 @@ namespace Microsoft.AspNet.Routing.Template public string GetVirtualPath(VirtualPathContext context) { - var values = _binder.GetAcceptedValues(context.AmbientValues, context.Values); + var values = _binder.GetValues(context.AmbientValues, context.Values); if (values == null) { // We're missing one of the required values for this route. @@ -111,7 +111,7 @@ namespace Microsoft.AspNet.Routing.Template } if (!RouteConstraintMatcher.Match(Constraints, - values, + values.CombinedValues, context.Context, this, RouteDirection.UrlGeneration)) @@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Routing.Template } // Validate that the target can accept these values. - var childContext = CreateChildVirtualPathContext(context, values); + var childContext = CreateChildVirtualPathContext(context, values.AcceptedValues); var path = _target.GetVirtualPath(childContext); if (path != null) { @@ -134,7 +134,7 @@ namespace Microsoft.AspNet.Routing.Template return null; } - path = _binder.BindValues(values); + path = _binder.BindValues(values.AcceptedValues); if (path != null) { context.IsBound = true; diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs b/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs new file mode 100644 index 0000000000..1876678fcb --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Template +{ + /// + /// The values used as inputs for constraints and link generation. + /// + public class TemplateValuesResult + { + /// + /// The set of values that will appear in the URL. + /// + public Dictionary AcceptedValues { get; set; } + + /// + /// The set of values that that were supplied for URL generation. + /// + /// + /// This combines implicit (ambient) values from the of the current request + /// (if applicable), explictly provided values, and default values for parameters that appear in + /// the route template. + /// + /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the + /// route template are excluded from this set. + /// + public Dictionary CombinedValues { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj index 7ab2d1633b..04c3acab62 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj +++ b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj @@ -43,7 +43,6 @@ - diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs index ae5fb835f0..9fdc2274fb 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -138,8 +138,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests defaults); // Act & Assert - var acceptedValues = binder.GetAcceptedValues(null, values); - if (acceptedValues == null) + var result = binder.GetValues(ambientValues: null, values: values); + if (result == null) { if (expected == null) { @@ -147,11 +147,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests } else { - Assert.NotNull(acceptedValues); + Assert.NotNull(result); } } - var boundTemplate = binder.BindValues(acceptedValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); if (expected == null) { Assert.Null(boundTemplate); @@ -971,8 +971,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests var binder = new TemplateBinder(TemplateParser.Parse(template, _inlineConstraintResolver), defaults); // Act & Assert - var acceptedValues = binder.GetAcceptedValues(ambientValues, values); - if (acceptedValues == null) + var result = binder.GetValues(ambientValues, values); + if (result == null) { if (expected == null) { @@ -980,11 +980,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests } else { - Assert.NotNull(acceptedValues); + Assert.NotNull(result); } } - var boundTemplate = binder.BindValues(acceptedValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); if (expected == null) { Assert.Null(boundTemplate); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 2409f3628e..df90bece69 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -364,6 +364,117 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal(expectedValues, childContext.ProvidedValues); } + // Any ambient values from the current request should be visible to constraint, even + // if they have nothing to do with the route generating a link + [Fact] + public void GetVirtualPath_ConstraintsSeeAmbientValues() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: null, + accept: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog", extra = "42" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", extra = "42" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("slug/Home/Store", path); + Assert.Equal(expectedValues, constraint.Values); + } + + // Non-parameter default values from the routing generating a link are not in the 'values' + // collection when constraints are processed. + [Fact] + public void GetVirtualPath_ConstraintsDontSeeDefaults_WhenTheyArentParameters() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17" }, + accept: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("slug/Home/Store", path); + Assert.Equal(expectedValues, constraint.Values); + } + + // Default values are visible to the constraint when they are used to fill a parameter. + [Fact] + public void GetVirtualPath_ConstraintsSeesDefault_WhenThereItsAParamter() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { action = "Index" }, + accept: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { controller = "Shopping" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Shopping", action = "Index" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("slug/Shopping", path); + Assert.Equal(expectedValues, constraint.Values); + } + + // Default values from the routing generating a link are in the 'values' collection when + // constraints are processed - IFF they are specified as values or ambient values. + [Fact] + public void GetVirtualPath_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17", thirdthing = "13" }, + accept: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store", thirdthing = "13" }, + ambientValues: new { Controller = "Home", action = "Blog", otherthing = "17" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("slug/Home/Store", path); + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } + private static VirtualPathContext CreateVirtualPathContext(object values) { return CreateVirtualPathContext(new RouteValueDictionary(values), null); @@ -520,12 +631,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests return new TemplateRoute(CreateTarget(accept), template, _inlineConstraintResolver); } - private static TemplateRoute CreateRoute(string template, object defaults, bool accept = true, IDictionary constraints = null) + private static TemplateRoute CreateRoute(string template, object defaults, bool accept = true, object constraints = null) { return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults), - constraints, + (constraints as IDictionary) ?? new RouteValueDictionary(constraints), _inlineConstraintResolver); } @@ -569,6 +680,22 @@ namespace Microsoft.AspNet.Routing.Template.Tests resolverMock.Setup(o => o.ResolveConstraint("int")).Returns(new IntRouteConstraint()); return resolverMock.Object; } + + private class CapturingConstraint : IRouteConstraint + { + public IDictionary Values { get; private set; } + + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + Values = new RouteValueDictionary(values); + return true; + } + } } }