Fix for #77 - pass ambient values not in the template to constraints
This change adds tests and makes the behavior consistent with legacy MVC as far as what values are visible in constraints. This is important because it allows constraints to make decisions based on whether or not a value is present even if it's not in the template. This is similar to the behavior of WebAPI link generation or Area link generation in MVC 5 - but without hardcoding.
This commit is contained in:
parent
c6eff50c24
commit
63dcdd6ca5
|
|
@ -70,6 +70,7 @@
|
|||
<Compile Include="Template\TemplatePart.cs" />
|
||||
<Compile Include="Template\TemplateRoute.cs" />
|
||||
<Compile Include="Template\TemplateSegment.cs" />
|
||||
<Compile Include="Template\TemplateValuesResult.cs" />
|
||||
<Compile Include="VirtualPathContext.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
|
|
|
|||
|
|
@ -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<string, object> GetAcceptedValues(IDictionary<string, object> ambientValues,
|
||||
public TemplateValuesResult GetValues(IDictionary<string, object> ambientValues,
|
||||
IDictionary<string, object> 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<string, object>(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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The values used as inputs for constraints and link generation.
|
||||
/// </summary>
|
||||
public class TemplateValuesResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The set of values that will appear in the URL.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> AcceptedValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The set of values that that were supplied for URL generation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This combines implicit (ambient) values from the <see cref="RouteData"/> 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.
|
||||
/// </remarks>
|
||||
public Dictionary<string, object> CombinedValues { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,6 @@
|
|||
<Compile Include="DefaultInlineConstraintResolverTest.cs" />
|
||||
<Compile Include="RouteCollectionTest.cs" />
|
||||
<Compile Include="InlineRouteParameterParserTests.cs" />
|
||||
<Compile Include="DefaultValueTests.cs" />
|
||||
<Compile Include="RouteOptionsTests.cs" />
|
||||
<Compile Include="RouteValueDictionaryTests.cs" />
|
||||
<Compile Include="TemplateParserDefaultValuesTests.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);
|
||||
|
|
|
|||
|
|
@ -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<string, object> 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<string, object>) ?? 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<string, object> Values { get; private set; }
|
||||
|
||||
public bool Match(
|
||||
HttpContext httpContext,
|
||||
IRouter route,
|
||||
string routeKey,
|
||||
IDictionary<string, object> values,
|
||||
RouteDirection routeDirection)
|
||||
{
|
||||
Values = new RouteValueDictionary(values);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue