From 87a47f50cc15d9ee50da496962de7a75235efa4e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 27 Mar 2014 14:27:16 -0700 Subject: [PATCH] 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. --- .../Template/TemplateRoute.cs | 52 ++++++++++-- .../VirtualPathContext.cs | 2 + .../Template/TemplateRouteTests.cs | 84 ++++++++++++++++++- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 873f6d8..d157fdb 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Threading.Tasks; namespace Microsoft.AspNet.Routing.Template @@ -11,6 +12,7 @@ namespace Microsoft.AspNet.Routing.Template private readonly IDictionary _defaults; private readonly IDictionary _constraints; private readonly IRouter _target; + private readonly Template _parsedTemplate; private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; @@ -29,10 +31,10 @@ namespace Microsoft.AspNet.Routing.Template _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); // The parser will throw for invalid routes. - var parsedTemplate = TemplateParser.Parse(RouteTemplate); + _parsedTemplate = TemplateParser.Parse(RouteTemplate); - _matcher = new TemplateMatcher(parsedTemplate); - _binder = new TemplateBinder(parsedTemplate, _defaults); + _matcher = new TemplateMatcher(_parsedTemplate); + _binder = new TemplateBinder(_parsedTemplate, _defaults); } public IDictionary Defaults @@ -85,7 +87,7 @@ namespace Microsoft.AspNet.Routing.Template var values = _binder.GetAcceptedValues(context.AmbientValues, context.Values); 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; } @@ -99,25 +101,59 @@ namespace Microsoft.AspNet.Routing.Template } // 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 the target generates a value then that can short circuit. + context.IsBound = true; return path; } - else if (!context.IsBound) + else if (!childContext.IsBound) { // The target has rejected these values. return null; } path = _binder.BindValues(values); - if (path == null) + if (path != null) { - context.IsBound = false; + context.IsBound = true; } return path; } + + private VirtualPathContext CreateChildVirtualPathContext( + VirtualPathContext context, + IDictionary 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, + }; + } } } diff --git a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs index fe1cfbb..2dfb3b5 100644 --- a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs +++ b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Routing Values = values; } + public IDictionary ProvidedValues { get; set; } + public IDictionary AmbientValues { get; private set; } public HttpContext Context { get; private set; } diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 3e631cf..eb7d351 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -284,6 +284,78 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal("hello/1234", virtualPath); } + [Fact] + public void GetVirtualPath_Sends_ProvidedValues() + { + // Arrange + VirtualPathContext childContext = null; + var target = new Mock(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Callback(c => { childContext = c; c.IsBound = true; }) + .Returns(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(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Callback(c => { childContext = c; c.IsBound = true; }) + .Returns(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(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Callback(c => { childContext = c; c.IsBound = true; }) + .Returns(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) { 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); } + 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) { var target = new Mock(MockBehavior.Strict); @@ -373,4 +455,4 @@ namespace Microsoft.AspNet.Routing.Template.Tests } } -#endif \ No newline at end of file +#endif