From 950ce56ea5ac2c815bbc98fad0e7a254dcd35e36 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Thu, 24 Apr 2014 14:58:31 -0700 Subject: [PATCH] Adding Support for NamedRoutes. - Interface Changes. - RouteCollectionExtensions - Tests for Named Routes --- samples/RoutingSample.Web/Startup.cs | 10 +- src/Microsoft.AspNet.Routing/INamedRouter.cs | 9 ++ .../Microsoft.AspNet.Routing.kproj | 1 + .../Properties/Resources.Designer.cs | 16 ++ src/Microsoft.AspNet.Routing/Resources.resx | 3 + .../RouteCollection.cs | 61 +++++++- .../RouteCollectionExtensions.cs | 30 ++-- .../Template/TemplateRoute.cs | 18 ++- .../VirtualPathContext.cs | 15 +- .../RouteCollectionTest.cs | 137 +++++++++++++++++- .../Template/TemplateRouteTests.cs | 45 +++++- 11 files changed, 310 insertions(+), 35 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/INamedRouter.cs diff --git a/samples/RoutingSample.Web/Startup.cs b/samples/RoutingSample.Web/Startup.cs index cd76ba9f64..5610d3601b 100644 --- a/samples/RoutingSample.Web/Startup.cs +++ b/samples/RoutingSample.Web/Startup.cs @@ -21,15 +21,17 @@ namespace RoutingSample.Web routes.DefaultHandler = endpoint1; routes.AddPrefixRoute("api/store"); - routes.MapRoute("api/constraint/{controller}", null, new { controller = "my.*" }); - routes.MapRoute("api/rconstraint/{controller}", + routes.MapRoute("defaultRoute", "api/constraint/{controller}", null, new { controller = "my.*" }); + routes.MapRoute("regexStringRoute", + "api/rconstraint/{controller}", new { foo = "Bar" }, new { controller = new RegexConstraint("^(my.*)$") }); - routes.MapRoute("api/r2constraint/{controller}", + routes.MapRoute("regexRoute", + "api/r2constraint/{controller}", new { foo = "Bar2" }, new { controller = new RegexConstraint(new Regex("^(my.*)$")) }); - routes.MapRoute("api/{controller}/{*extra}", new { controller = "Store" }); + routes.MapRoute("parameterConstraintRoute", "api/{controller}/{*extra}", new { controller = "Store" }); routes.AddPrefixRoute("hello/world", endpoint2); routes.AddPrefixRoute("", endpoint2); diff --git a/src/Microsoft.AspNet.Routing/INamedRouter.cs b/src/Microsoft.AspNet.Routing/INamedRouter.cs new file mode 100644 index 0000000000..891a816e82 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/INamedRouter.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface INamedRouter : IRouter + { + string Name { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj index c6fd49fc0e..b6cfaa410f 100644 --- a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj +++ b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj @@ -22,6 +22,7 @@ + diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs index cbfe5d98cd..cfc9ad76fe 100644 --- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNet.Routing private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The supplied route name '{0}' is ambiguous and matched more than one routes. + /// + internal static string NamedRoutes_AmbiguousRoutesFound + { + get { return GetString("NamedRoutes_AmbiguousRoutesFound"); } + } + + /// + /// The supplied route name '{0}' is ambiguous and matched more than one routes. + /// + internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("NamedRoutes_AmbiguousRoutesFound"), p0); + } + /// /// A default handler must be set on the RouteCollection. /// diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index b7bd0bf538..0f73bb2cd9 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The supplied route name '{0}' is ambiguous and matched more than one route. + A default handler must be set on the RouteCollection. diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index 42a3363322..860fdfe506 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -1,4 +1,6 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -7,6 +9,9 @@ namespace Microsoft.AspNet.Routing public class RouteCollection : IRouteCollection { private readonly List _routes = new List(); + private readonly List _unnamedRoutes = new List(); + private readonly Dictionary _namedRoutes = + new Dictionary(StringComparer.OrdinalIgnoreCase); public IRouter this[int index] { @@ -20,8 +25,21 @@ namespace Microsoft.AspNet.Routing public IRouter DefaultHandler { get; set; } - public void Add(IRouter router) + public void Add([NotNull] IRouter router) { + var namedRouter = router as INamedRouter; + if (namedRouter != null) + { + if (!string.IsNullOrEmpty(namedRouter.Name)) + { + _namedRoutes.Add(namedRouter.Name, namedRouter); + } + } + else + { + _unnamedRoutes.Add(router); + } + _routes.Add(router); } @@ -41,18 +59,45 @@ namespace Microsoft.AspNet.Routing public virtual string GetVirtualPath(VirtualPathContext context) { - for (var i = 0; i < Count; i++) + if (!string.IsNullOrEmpty(context.RouteName)) { - var route = this[i]; + INamedRouter matchedNamedRoute; + _namedRoutes.TryGetValue(context.RouteName, out matchedNamedRoute); - var path = route.GetVirtualPath(context); - if (path != null) + var virtualPath = matchedNamedRoute != null ? matchedNamedRoute.GetVirtualPath(context) : null; + foreach (var unnamedRoute in _unnamedRoutes) { - return path; + var tempVirtualPath = unnamedRoute.GetVirtualPath(context); + if (tempVirtualPath != null) + { + if (virtualPath != null) + { + // There was already a previous route which matched the name. + throw new InvalidOperationException( + Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName)); + } + + virtualPath = tempVirtualPath; + } + } + + return virtualPath; + } + else + { + for (var i = 0; i < Count; i++) + { + var route = this[i]; + + var path = route.GetVirtualPath(context); + if (path != null) + { + return path; + } } } return null; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs index a5cfdf9db3..4b1550da38 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs @@ -8,20 +8,20 @@ namespace Microsoft.AspNet.Routing { public static class RouteCollectionExtensions { - public static IRouteCollection MapRoute(this IRouteCollection routes, string template) + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template) { - MapRoute(routes, template, defaults: null); + MapRoute(routes, name, template, defaults: null); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, object defaults) { - MapRoute(routes, template, new RouteValueDictionary(defaults)); + MapRoute(routes, name, template, new RouteValueDictionary(defaults)); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, IDictionary defaults) { if (routes.DefaultHandler == null) @@ -29,32 +29,32 @@ namespace Microsoft.AspNet.Routing throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); } - routes.Add(new TemplateRoute(routes.DefaultHandler, template, defaults, constraints: null)); + routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints: null)); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, - object defaults, object constraints) + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, + object defaults, object constraints) { - MapRoute(routes, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); + MapRoute(routes, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, object defaults, IDictionary constraints) { - MapRoute(routes, template, new RouteValueDictionary(defaults), constraints); + MapRoute(routes, name, template, new RouteValueDictionary(defaults), constraints); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, IDictionary defaults, object constraints) { - MapRoute(routes, template, defaults, new RouteValueDictionary(constraints)); + MapRoute(routes, name, template, defaults, new RouteValueDictionary(constraints)); return routes; } - public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + public static IRouteCollection MapRoute(this IRouteCollection routes, string name, string template, IDictionary defaults, IDictionary constraints) { if (routes.DefaultHandler == null) @@ -62,7 +62,7 @@ namespace Microsoft.AspNet.Routing throw new InvalidOperationException(Resources.DefaultHandler_MustBeSet); } - routes.Add(new TemplateRoute(routes.DefaultHandler, template, defaults, constraints)); + routes.Add(new TemplateRoute(routes.DefaultHandler, name, template, defaults, constraints)); return routes; } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index d157fdb2e0..7524e63270 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Routing.Template { - public class TemplateRoute : IRouter + public class TemplateRoute : INamedRouter { private readonly IDictionary _defaults; private readonly IDictionary _constraints; @@ -22,11 +22,23 @@ namespace Microsoft.AspNet.Routing.Template { } - public TemplateRoute([NotNull] IRouter target, string routeTemplate, IDictionary defaults, + public TemplateRoute([NotNull] IRouter target, + string routeTemplate, + IDictionary defaults, + IDictionary constraints) + : this(target, null, routeTemplate, defaults, constraints) + { + } + + public TemplateRoute([NotNull] IRouter target, + string routeName, + string routeTemplate, + IDictionary defaults, IDictionary constraints) { _target = target; _routeTemplate = routeTemplate ?? string.Empty; + Name = routeName; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); @@ -37,6 +49,8 @@ namespace Microsoft.AspNet.Routing.Template _binder = new TemplateBinder(_parsedTemplate, _defaults); } + public string Name { get; private set; } + public IDictionary Defaults { get { return _defaults; } diff --git a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs index 2dfb3b5769..ed8a0a6616 100644 --- a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs +++ b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs @@ -7,13 +7,26 @@ namespace Microsoft.AspNet.Routing { public class VirtualPathContext { - public VirtualPathContext(HttpContext context, IDictionary ambientValues, IDictionary values) + public VirtualPathContext(HttpContext httpContext, + IDictionary ambientValues, + IDictionary values) + : this(httpContext, ambientValues, values, null) + { + } + + public VirtualPathContext(HttpContext context, + IDictionary ambientValues, + IDictionary values, + string routeName) { Context = context; AmbientValues = ambientValues; Values = values; + RouteName = routeName; } + public string RouteName { get; private set; } + public IDictionary ProvidedValues { get; set; } public IDictionary AmbientValues { get; private set; } diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs index 94a3a8b34c..e78707e530 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs @@ -1,6 +1,8 @@ - -#if NET45 +#if NET45 +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Moq; @@ -79,6 +81,136 @@ namespace Microsoft.AspNet.Routing.Tests Assert.False(context.IsHandled); } + [Fact] + public void NamedRouteTests_GetNamedRoute_ReturnsValue() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" }); + var virtualPathContext = CreateVirtualPathContext("RouteName"); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal("RouteName", stringVirtualPath); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_RouteNotFound() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3" }); + var virtualPathContext = CreateVirtualPathContext("NonExistantRoute"); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Null(stringVirtualPath); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3", "Route4" }); + + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute("Route3")); + var virtualPathContext = CreateVirtualPathContext("Route1"); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal("Route1", stringVirtualPath); + } + + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute() + { + // Arrange + var ambiguousRoute = "ambiguousRoute"; + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", ambiguousRoute, "Route4" }); + + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute(ambiguousRoute)); + var virtualPathContext = CreateVirtualPathContext(ambiguousRoute); + + // Act & Assert + var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); + Assert.Equal("The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", ex.Message); + } + + private static RouteCollection GetRouteCollectionWithNamedRoutes(IEnumerable routeNames) + { + var routes = new RouteCollection(); + foreach (var routeName in routeNames) + { + var route1 = CreateNamedRoute(routeName); + routes.Add(route1); + } + + return routes; + } + + private static RouteCollection GetNestedRouteCollection(string[] routeNames) + { + var rnd = new Random(); + int index = rnd.Next(0, routeNames.Length - 1); + var first = routeNames.Take(index).ToArray(); + var second = routeNames.Skip(index).ToArray(); + + var rc1 = GetRouteCollectionWithNamedRoutes(first); + var rc2 = GetRouteCollectionWithNamedRoutes(second); + var rc3 = new RouteCollection(); + var rc4 = new RouteCollection(); + + rc1.Add(rc3); + rc4.Add(rc2); + + // Add a few unnamedRoutes. + rc1.Add(CreateRoute().Object); + rc2.Add(CreateRoute().Object); + rc3.Add(CreateRoute().Object); + rc3.Add(CreateRoute().Object); + rc4.Add(CreateRoute().Object); + rc4.Add(CreateRoute().Object); + + var routeCollection = new RouteCollection(); + routeCollection.Add(rc1); + routeCollection.Add(rc4); + + return routeCollection; + } + + private static INamedRouter CreateNamedRoute(string name, bool accept = false) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Callback(c => c.IsBound = accept) + .Returns(c => name) + .Verifiable(); + + target + .SetupGet(e => e.Name) + .Returns(name); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback(async (c) => c.IsHandled = accept) + .Returns(Task.FromResult(null)) + .Verifiable(); + + return target.Object; + } + + private static VirtualPathContext CreateVirtualPathContext(string routeName) + { + return new VirtualPathContext(null, null, null, routeName); + } + private static RouteContext CreateRouteContext(string requestPath) { var request = new Mock(MockBehavior.Strict); @@ -104,7 +236,6 @@ namespace Microsoft.AspNet.Routing.Tests .Callback(async (c) => c.IsHandled = accept) .Returns(Task.FromResult(null)) .Verifiable(); - return target; } diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 689da6c1dd..0099b047dc 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -375,6 +375,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests return new VirtualPathContext(context.Object, ambientValues, values); } + private static VirtualPathContext CreateVirtualPathContext(string routeName) + { + return new VirtualPathContext(null, null, null, routeName); + } + #endregion #region Route Registration @@ -387,7 +392,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests collection.DefaultHandler = new Mock().Object; // Assert - ExceptionAssert.Throws(() => collection.MapRoute("{controller}/{action}", + ExceptionAssert.Throws(() => collection.MapRoute("mockName", + "{controller}/{action}", defaults: null, constraints: new { controller = "a.*", action = new Object() }), "The constraint entry 'action' on the route with route template '{controller}/{action}' " + @@ -404,7 +410,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests var mockConstraint = new Mock().Object; - collection.MapRoute("{controller}/{action}", + collection.MapRoute("mockName", + "{controller}/{action}", defaults: null, constraints: new { controller = "a.*", action = mockConstraint }); @@ -414,7 +421,41 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal(2, constraints.Count); Assert.IsType(constraints["controller"]); Assert.Equal(mockConstraint, constraints["action"]); + } + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + + collection.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null); + + // Act + var name = ((TemplateRoute)collection[0]).Name; + + // Assert + Assert.Equal("RouteName", name); + } + + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaultsAndConstraints_AddsTheRoute() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + + collection.MapRoute(name: "RouteName", + template: "{controller}/{action}", + defaults: null, + constraints: null); + + // Act + var name = ((TemplateRoute)collection[0]).Name; + + // Assert + Assert.Equal("RouteName", name); } #endregion