Adding Support for NamedRoutes.

- Interface Changes.
- RouteCollectionExtensions
- Tests for Named Routes
This commit is contained in:
harshgMSFT 2014-04-24 14:58:31 -07:00
parent f86877b14f
commit 950ce56ea5
11 changed files with 310 additions and 35 deletions

View File

@ -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);

View File

@ -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; }
}
}

View File

@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="BuilderExtensions.cs" />
<Compile Include="INamedRouter.cs" />
<Compile Include="IRouteCollection.cs" />
<Compile Include="IRouteConstraint.cs" />
<Compile Include="IRouter.cs" />

View File

@ -10,6 +10,22 @@ namespace Microsoft.AspNet.Routing
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// The supplied route name '{0}' is ambiguous and matched more than one routes.
/// </summary>
internal static string NamedRoutes_AmbiguousRoutesFound
{
get { return GetString("NamedRoutes_AmbiguousRoutesFound"); }
}
/// <summary>
/// The supplied route name '{0}' is ambiguous and matched more than one routes.
/// </summary>
internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("NamedRoutes_AmbiguousRoutesFound"), p0);
}
/// <summary>
/// A default handler must be set on the RouteCollection.
/// </summary>

View File

@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NamedRoutes_AmbiguousRoutesFound" xml:space="preserve">
<value>The supplied route name '{0}' is ambiguous and matched more than one route.</value>
</data>
<data name="DefaultHandler_MustBeSet" xml:space="preserve">
<value>A default handler must be set on the RouteCollection.</value>
</data>

View File

@ -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<IRouter> _routes = new List<IRouter>();
private readonly List<IRouter> _unnamedRoutes = new List<IRouter>();
private readonly Dictionary<string, INamedRouter> _namedRoutes =
new Dictionary<string, INamedRouter>(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;
}
}
}
}

View File

@ -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<string, object> 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<string, object> 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<string, object> 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<string, object> defaults, IDictionary<string, object> 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;
}
}

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Routing.Template
{
public class TemplateRoute : IRouter
public class TemplateRoute : INamedRouter
{
private readonly IDictionary<string, object> _defaults;
private readonly IDictionary<string, IRouteConstraint> _constraints;
@ -22,11 +22,23 @@ namespace Microsoft.AspNet.Routing.Template
{
}
public TemplateRoute([NotNull] IRouter target, string routeTemplate, IDictionary<string, object> defaults,
public TemplateRoute([NotNull] IRouter target,
string routeTemplate,
IDictionary<string, object> defaults,
IDictionary<string, object> constraints)
: this(target, null, routeTemplate, defaults, constraints)
{
}
public TemplateRoute([NotNull] IRouter target,
string routeName,
string routeTemplate,
IDictionary<string, object> defaults,
IDictionary<string, object> constraints)
{
_target = target;
_routeTemplate = routeTemplate ?? string.Empty;
Name = routeName;
_defaults = defaults ?? new Dictionary<string, object>(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<string, object> Defaults
{
get { return _defaults; }

View File

@ -7,13 +7,26 @@ namespace Microsoft.AspNet.Routing
{
public class VirtualPathContext
{
public VirtualPathContext(HttpContext context, IDictionary<string, object> ambientValues, IDictionary<string, object> values)
public VirtualPathContext(HttpContext httpContext,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values)
: this(httpContext, ambientValues, values, null)
{
}
public VirtualPathContext(HttpContext context,
IDictionary<string, object> ambientValues,
IDictionary<string, object> values,
string routeName)
{
Context = context;
AmbientValues = ambientValues;
Values = values;
RouteName = routeName;
}
public string RouteName { get; private set; }
public IDictionary<string, object> ProvidedValues { get; set; }
public IDictionary<string, object> AmbientValues { get; private set; }

View File

@ -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<InvalidOperationException>(() => 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<string> 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<INamedRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => c.IsBound = accept)
.Returns<VirtualPathContext>(c => name)
.Verifiable();
target
.SetupGet(e => e.Name)
.Returns(name);
target
.Setup(e => e.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(async (c) => c.IsHandled = accept)
.Returns(Task.FromResult<object>(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<HttpRequest>(MockBehavior.Strict);
@ -104,7 +236,6 @@ namespace Microsoft.AspNet.Routing.Tests
.Callback<RouteContext>(async (c) => c.IsHandled = accept)
.Returns(Task.FromResult<object>(null))
.Verifiable();
return target;
}

View File

@ -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<IRouter>().Object;
// Assert
ExceptionAssert.Throws<InvalidOperationException>(() => collection.MapRoute("{controller}/{action}",
ExceptionAssert.Throws<InvalidOperationException>(() => 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<IRouteConstraint>().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<RegexConstraint>(constraints["controller"]);
Assert.Equal(mockConstraint, constraints["action"]);
}
[Fact]
public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute()
{
// Arrange
var collection = new RouteCollection();
collection.DefaultHandler = new Mock<IRouter>().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<IRouter>().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