Passing more data to on GetVirtualPath

For link generation to areas, we need to provide the set of values that
the route could potentially provide.

Basically if we know what action we want to reach, we want to know whether
or not a given route could hit that action before giving it the OK to
generate a link.

For instance a route like '{controller}' couldn't hit an action like
'HomeController:DoACoolThing', since it can never provide a value for
'action'. This makes it possible for WebFX to make the right decision
without changing the behavior of any of the routing constructs. This also
has the side-effect of removing a class of order dependencies in routing
that cause bad links to be generated.
This commit is contained in:
Ryan Nowak 2014-03-27 14:27:16 -07:00
Родитель 90864bcd9c
Коммит 87a47f50cc
3 изменённых файлов: 129 добавлений и 9 удалений

Просмотреть файл

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
@ -11,6 +12,7 @@ namespace Microsoft.AspNet.Routing.Template
private readonly IDictionary<string, object> _defaults; private readonly IDictionary<string, object> _defaults;
private readonly IDictionary<string, IRouteConstraint> _constraints; private readonly IDictionary<string, IRouteConstraint> _constraints;
private readonly IRouter _target; private readonly IRouter _target;
private readonly Template _parsedTemplate;
private readonly string _routeTemplate; private readonly string _routeTemplate;
private readonly TemplateMatcher _matcher; private readonly TemplateMatcher _matcher;
private readonly TemplateBinder _binder; private readonly TemplateBinder _binder;
@ -29,10 +31,10 @@ namespace Microsoft.AspNet.Routing.Template
_constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate);
// The parser will throw for invalid routes. // The parser will throw for invalid routes.
var parsedTemplate = TemplateParser.Parse(RouteTemplate); _parsedTemplate = TemplateParser.Parse(RouteTemplate);
_matcher = new TemplateMatcher(parsedTemplate); _matcher = new TemplateMatcher(_parsedTemplate);
_binder = new TemplateBinder(parsedTemplate, _defaults); _binder = new TemplateBinder(_parsedTemplate, _defaults);
} }
public IDictionary<string, object> Defaults public IDictionary<string, object> Defaults
@ -85,7 +87,7 @@ namespace Microsoft.AspNet.Routing.Template
var values = _binder.GetAcceptedValues(context.AmbientValues, context.Values); var values = _binder.GetAcceptedValues(context.AmbientValues, context.Values);
if (values == null) if (values == null)
{ {
// We're missing one the required values for this route. // We're missing one of the required values for this route.
return null; return null;
} }
@ -99,25 +101,59 @@ namespace Microsoft.AspNet.Routing.Template
} }
// Validate that the target can accept these values. // Validate that the target can accept these values.
var path = _target.GetVirtualPath(context); var childContext = CreateChildVirtualPathContext(context, values);
var path = _target.GetVirtualPath(childContext);
if (path != null) if (path != null)
{ {
// If the target generates a value then that can short circuit. // If the target generates a value then that can short circuit.
context.IsBound = true;
return path; return path;
} }
else if (!context.IsBound) else if (!childContext.IsBound)
{ {
// The target has rejected these values. // The target has rejected these values.
return null; return null;
} }
path = _binder.BindValues(values); path = _binder.BindValues(values);
if (path == null) if (path != null)
{ {
context.IsBound = false; context.IsBound = true;
} }
return path; return path;
} }
private VirtualPathContext CreateChildVirtualPathContext(
VirtualPathContext context,
IDictionary<string, object> acceptedValues)
{
// We want to build the set of values that would be provided if this route were to generated
// a link and then immediately match it. This includes all the accepted parameter values, and
// the defaults. Accepted values that would go in the query string aren't included.
var providedValues = new RouteValueDictionary();
foreach (var parameter in _parsedTemplate.Parameters)
{
object value;
if (acceptedValues.TryGetValue(parameter.Name, out value))
{
providedValues.Add(parameter.Name, value);
}
}
foreach (var kvp in _defaults)
{
if (!providedValues.ContainsKey(kvp.Key))
{
providedValues.Add(kvp.Key, kvp.Value);
}
}
return new VirtualPathContext(context.Context, context.AmbientValues, context.Values)
{
ProvidedValues = providedValues,
};
}
} }
} }

Просмотреть файл

@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Routing
Values = values; Values = values;
} }
public IDictionary<string, object> ProvidedValues { get; set; }
public IDictionary<string, object> AmbientValues { get; private set; } public IDictionary<string, object> AmbientValues { get; private set; }
public HttpContext Context { get; private set; } public HttpContext Context { get; private set; }

Просмотреть файл

@ -284,6 +284,78 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal("hello/1234", virtualPath); Assert.Equal("hello/1234", virtualPath);
} }
[Fact]
public void GetVirtualPath_Sends_ProvidedValues()
{
// Arrange
VirtualPathContext childContext = null;
var target = new Mock<IRouter>(MockBehavior.Strict);
target
.Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => { childContext = c; c.IsBound = true; })
.Returns<string>(null);
var route = CreateRoute(target.Object, "{controller}/{action}");
var context = CreateVirtualPathContext(new { action = "Store" }, new { Controller = "Home", action = "Blog"});
var expectedValues = new RouteValueDictionary(new {controller = "Home", action = "Store"});
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Store", path);
Assert.Equal(expectedValues, childContext.ProvidedValues);
}
[Fact]
public void GetVirtualPath_Sends_ProvidedValues_IncludingDefaults()
{
// Arrange
VirtualPathContext childContext = null;
var target = new Mock<IRouter>(MockBehavior.Strict);
target
.Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => { childContext = c; c.IsBound = true; })
.Returns<string>(null);
var route = CreateRoute(target.Object, "Admin/{controller}/{action}", new {area = "Admin"});
var context = CreateVirtualPathContext(new { action = "Store" }, new { Controller = "Home", action = "Blog" });
var expectedValues = new RouteValueDictionary(new { controller = "Home", action = "Store", area = "Admin" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Admin/Home/Store", path);
Assert.Equal(expectedValues, childContext.ProvidedValues);
}
[Fact]
public void GetVirtualPath_Sends_ProvidedValues_ButNotQueryStringValues()
{
// Arrange
VirtualPathContext childContext = null;
var target = new Mock<IRouter>(MockBehavior.Strict);
target
.Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => { childContext = c; c.IsBound = true; })
.Returns<string>(null);
var route = CreateRoute(target.Object, "{controller}/{action}");
var context = CreateVirtualPathContext(new { action = "Store", id = 5 }, new { Controller = "Home", action = "Blog" });
var expectedValues = new RouteValueDictionary(new { controller = "Home", action = "Store" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Store?id=5", path);
Assert.Equal(expectedValues, childContext.ProvidedValues);
}
private static VirtualPathContext CreateVirtualPathContext(object values) private static VirtualPathContext CreateVirtualPathContext(object values)
{ {
return CreateVirtualPathContext(new RouteValueDictionary(values), null); return CreateVirtualPathContext(new RouteValueDictionary(values), null);
@ -355,6 +427,16 @@ namespace Microsoft.AspNet.Routing.Template.Tests
return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults), constraints); return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults), constraints);
} }
private static TemplateRoute CreateRoute(IRouter target, string template)
{
return new TemplateRoute(target, template, new RouteValueDictionary(), constraints: null);
}
private static TemplateRoute CreateRoute(IRouter target, string template, object defaults)
{
return new TemplateRoute(target, template, new RouteValueDictionary(defaults), constraints: null);
}
private static IRouter CreateTarget(bool accept = true) private static IRouter CreateTarget(bool accept = true)
{ {
var target = new Mock<IRouter>(MockBehavior.Strict); var target = new Mock<IRouter>(MockBehavior.Strict);
@ -373,4 +455,4 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
} }
#endif #endif