Add support for best-effort link-generation
This change adds a feature needed for aspnet/Mvc#302 There's a new option in routing that allows link-generation to proceed when the route values cannot be validated. The key scenario for this is during development of an MVC site. Routing will refuse to generate a link to actions which don't exists, this is a breaking change from the MVC5 behavior. Setting UseBestEffortLinkGeneration will allow routing to return a value even when we can't match the action. This option will remain off by default - setting this to on will impact link-generation in a bunch of scenarios involving areas where we've improved the logic for MVC6. If you're considering leaving this on outside of development scenarios, then make sure to be as explicit with route values as possible (don't rely on ambient values). Functional tests to follow up in the MVC repo.
This commit is contained in:
parent
4e5fc2e2dd
commit
9ee946073a
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Routing.Logging;
|
using Microsoft.AspNet.Routing.Logging;
|
||||||
using Microsoft.AspNet.Routing.Logging.Internal;
|
using Microsoft.AspNet.Routing.Logging.Internal;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Routing
|
namespace Microsoft.AspNet.Routing
|
||||||
|
|
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Routing
|
||||||
private readonly List<IRouter> _unnamedRoutes = new List<IRouter>();
|
private readonly List<IRouter> _unnamedRoutes = new List<IRouter>();
|
||||||
private readonly Dictionary<string, INamedRouter> _namedRoutes =
|
private readonly Dictionary<string, INamedRouter> _namedRoutes =
|
||||||
new Dictionary<string, INamedRouter>(StringComparer.OrdinalIgnoreCase);
|
new Dictionary<string, INamedRouter>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
private RouteOptions _options;
|
||||||
|
|
||||||
public IRouter this[int index]
|
public IRouter this[int index]
|
||||||
{
|
{
|
||||||
|
|
@ -94,45 +98,103 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
public virtual string GetVirtualPath(VirtualPathContext context)
|
public virtual string GetVirtualPath(VirtualPathContext context)
|
||||||
{
|
{
|
||||||
|
EnsureOptions(context.Context);
|
||||||
|
|
||||||
|
// If we're using Best-Effort link generation then it means that we'll first look for a route where
|
||||||
|
// the route values are validated (context.IsBound == true). If we can't find a match like that, then
|
||||||
|
// we'll return the path from the first route to return one.
|
||||||
|
var useBestEffort = _options.UseBestEffortLinkGeneration;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(context.RouteName))
|
if (!string.IsNullOrEmpty(context.RouteName))
|
||||||
{
|
{
|
||||||
|
var isValidated = false;
|
||||||
|
string bestPath = null;
|
||||||
INamedRouter matchedNamedRoute;
|
INamedRouter matchedNamedRoute;
|
||||||
_namedRoutes.TryGetValue(context.RouteName, out matchedNamedRoute);
|
if (_namedRoutes.TryGetValue(context.RouteName, out matchedNamedRoute))
|
||||||
|
{
|
||||||
|
bestPath = matchedNamedRoute.GetVirtualPath(context);
|
||||||
|
isValidated = context.IsBound;
|
||||||
|
}
|
||||||
|
|
||||||
var virtualPath = matchedNamedRoute != null ? matchedNamedRoute.GetVirtualPath(context) : null;
|
// If we get here and context.IsBound == true, then we know we have a match, we want to keep
|
||||||
|
// iterating to see if we have multiple matches.
|
||||||
foreach (var unnamedRoute in _unnamedRoutes)
|
foreach (var unnamedRoute in _unnamedRoutes)
|
||||||
{
|
{
|
||||||
var tempVirtualPath = unnamedRoute.GetVirtualPath(context);
|
// reset because we're sharing the context
|
||||||
if (tempVirtualPath != null)
|
context.IsBound = false;
|
||||||
{
|
|
||||||
if (virtualPath != null)
|
|
||||||
{
|
|
||||||
// There was already a previous route which matched the name.
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName));
|
|
||||||
}
|
|
||||||
|
|
||||||
virtualPath = tempVirtualPath;
|
var path = unnamedRoute.GetVirtualPath(context);
|
||||||
|
if (path == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestPath != null)
|
||||||
|
{
|
||||||
|
// There was already a previous route which matched the name.
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName));
|
||||||
|
}
|
||||||
|
else if (context.IsBound)
|
||||||
|
{
|
||||||
|
// This is the first 'validated' match that we've found.
|
||||||
|
bestPath = path;
|
||||||
|
isValidated = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(bestPath == null);
|
||||||
|
|
||||||
|
// This is the first 'unvalidated' match that we've found.
|
||||||
|
bestPath = path;
|
||||||
|
isValidated = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return virtualPath;
|
if (isValidated || useBestEffort)
|
||||||
|
{
|
||||||
|
context.IsBound = isValidated;
|
||||||
|
return bestPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
string bestPath = null;
|
||||||
for (var i = 0; i < Count; i++)
|
for (var i = 0; i < Count; i++)
|
||||||
{
|
{
|
||||||
var route = this[i];
|
var route = this[i];
|
||||||
|
|
||||||
var path = route.GetVirtualPath(context);
|
var path = route.GetVirtualPath(context);
|
||||||
if (path != null)
|
if (path == null)
|
||||||
{
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.IsBound)
|
||||||
|
{
|
||||||
|
// This route has validated route values, short circuit.
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
else if (bestPath == null)
|
||||||
|
{
|
||||||
|
// The values aren't validated, but this is the best we've seen so far
|
||||||
|
bestPath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useBestEffort)
|
||||||
|
{
|
||||||
|
return bestPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureLogger(HttpContext context)
|
private void EnsureLogger(HttpContext context)
|
||||||
|
|
@ -143,5 +205,13 @@ namespace Microsoft.AspNet.Routing
|
||||||
_logger = factory.Create<RouteCollection>();
|
_logger = factory.Create<RouteCollection>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureOptions(HttpContext context)
|
||||||
|
{
|
||||||
|
if (_options == null)
|
||||||
|
{
|
||||||
|
_options = context.RequestServices.GetRequiredService<IOptions<RouteOptions>>().Options;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,5 +61,13 @@ namespace Microsoft.AspNet.Routing
|
||||||
{"required", typeof(RequiredRouteConstraint) },
|
{"required", typeof(RequiredRouteConstraint) },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value that enables best-effort link generation.
|
||||||
|
///
|
||||||
|
/// If enabled, link generation will use allow link generation to succeed when the set of values provided
|
||||||
|
/// cannot be validated.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseBestEffortLinkGeneration { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,24 +202,18 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
|
|
||||||
// Validate that the target can accept these values.
|
// Validate that the target can accept these values.
|
||||||
var childContext = CreateChildVirtualPathContext(context, values.AcceptedValues);
|
var childContext = CreateChildVirtualPathContext(context, values.AcceptedValues);
|
||||||
|
|
||||||
var path = _target.GetVirtualPath(childContext);
|
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 (!childContext.IsBound)
|
|
||||||
{
|
|
||||||
// The target has rejected these values.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// If we can produce a value go ahead and do it, the caller can check context.IsBound
|
||||||
|
// to see if the values were validated.
|
||||||
path = _binder.BindValues(values.AcceptedValues);
|
path = _binder.BindValues(values.AcceptedValues);
|
||||||
if (path != null)
|
context.IsBound = childContext.IsBound;
|
||||||
{
|
|
||||||
context.IsBound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +276,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
foreach (var inlineConstraint in parameter.InlineConstraints)
|
foreach (var inlineConstraint in parameter.InlineConstraints)
|
||||||
{
|
{
|
||||||
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
|
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return constraintBuilder.Build();
|
return constraintBuilder.Build();
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Routing.Logging;
|
using Microsoft.AspNet.Routing.Logging;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -231,6 +232,300 @@ namespace Microsoft.AspNet.Routing
|
||||||
Assert.Equal("The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", ex.Message);
|
Assert.Equal("The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_AmbiguousRoutes_RequiresRouteValueValidation_Error()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var namedRoute = CreateNamedRoute("Ambiguous", accept: false);
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(namedRoute);
|
||||||
|
|
||||||
|
var innerRouteCollection = new RouteCollection();
|
||||||
|
innerRouteCollection.Add(namedRoute);
|
||||||
|
routeCollection.Add(innerRouteCollection);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext("Ambiguous", options: options);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
|
||||||
|
Assert.Equal("The supplied route name 'Ambiguous' is ambiguous and matched more than one route.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_NamedRoute_BestEffort_BestInTopCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bestMatch = CreateNamedRoute("Match", accept: true, matchValue: "best");
|
||||||
|
var noMatch = CreateNamedRoute("NoMatch", accept: true, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(bestMatch);
|
||||||
|
|
||||||
|
var innerRouteCollection = new RouteCollection();
|
||||||
|
innerRouteCollection.Add(noMatch);
|
||||||
|
routeCollection.Add(innerRouteCollection);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext("Match", options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_NamedRoute_BestEffort_BestMatchInNestedCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bestMatch = CreateNamedRoute("NoMatch", accept: true, matchValue: "bad");
|
||||||
|
var noMatch = CreateNamedRoute("Match", accept: true, matchValue: "best");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(noMatch);
|
||||||
|
|
||||||
|
var innerRouteCollection = new RouteCollection();
|
||||||
|
innerRouteCollection.Add(bestMatch);
|
||||||
|
routeCollection.Add(innerRouteCollection);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext("Match", options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_NamedRoute_BestEffort_FirstRouteWins()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bestMatch = CreateNamedRoute("Match", accept: false, matchValue: "best");
|
||||||
|
var noMatch = CreateNamedRoute("NoMatch", accept: false, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(noMatch);
|
||||||
|
|
||||||
|
var innerRouteCollection = new RouteCollection();
|
||||||
|
innerRouteCollection.Add(bestMatch);
|
||||||
|
routeCollection.Add(innerRouteCollection);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext("Match", options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_BestEffort_FirstRouteWins()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: true, matchValue: "best");
|
||||||
|
var route2 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route3 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
routeCollection.Add(route2.Object);
|
||||||
|
routeCollection.Add(route3.Object);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
|
||||||
|
// All of these should be called
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_NoBestEffort_NoMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: true, matchValue: "best");
|
||||||
|
var route2 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route3 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
routeCollection.Add(route2.Object);
|
||||||
|
routeCollection.Add(route3.Object);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Null(path);
|
||||||
|
|
||||||
|
// All of these should be called
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_BestEffort_FirstRouteWins_WithNonMatchingRoutes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: false, matchValue: "bad");
|
||||||
|
var route2 = CreateRoute(accept: false, match: true, matchValue: "best");
|
||||||
|
var route3 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
routeCollection.Add(route2.Object);
|
||||||
|
routeCollection.Add(route3.Object);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
|
||||||
|
// All of these should be called
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_BestEffort_FirstValidatedValuesWins()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route2 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route3 = CreateRoute(accept: true, match: true, matchValue: "best");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
routeCollection.Add(route2.Object);
|
||||||
|
routeCollection.Add(route3.Object);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
|
||||||
|
// All of these should be called
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_BestEffort_FirstValidatedValuesWins_ShortCircuit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route2 = CreateRoute(accept: true, match: true, matchValue: "best");
|
||||||
|
var route3 = CreateRoute(accept: true, match: true, matchValue: "bad");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
routeCollection.Add(route2.Object);
|
||||||
|
routeCollection.Add(route3.Object);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVirtualPath_BestEffort_FirstValidatedValuesWins_Nested()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var route1 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route2 = CreateRoute(accept: false, match: true, matchValue: "bad");
|
||||||
|
var route3 = CreateRoute(accept: true, match: true, matchValue: "best");
|
||||||
|
|
||||||
|
var routeCollection = new RouteCollection();
|
||||||
|
routeCollection.Add(route1.Object);
|
||||||
|
|
||||||
|
var innerRouteCollection = new RouteCollection();
|
||||||
|
innerRouteCollection.Add(route2.Object);
|
||||||
|
innerRouteCollection.Add(route3.Object);
|
||||||
|
routeCollection.Add(innerRouteCollection);
|
||||||
|
|
||||||
|
var options = new RouteOptions()
|
||||||
|
{
|
||||||
|
UseBestEffortLinkGeneration = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var virtualPathContext = CreateVirtualPathContext(options: options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = routeCollection.GetVirtualPath(virtualPathContext);
|
||||||
|
|
||||||
|
Assert.Equal("best", path);
|
||||||
|
|
||||||
|
// All of these should be called
|
||||||
|
route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<TestSink> SetUp(bool enabled, bool handled)
|
private static async Task<TestSink> SetUp(bool enabled, bool handled)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -256,7 +551,7 @@ namespace Microsoft.AspNet.Routing
|
||||||
var routes = new RouteCollection();
|
var routes = new RouteCollection();
|
||||||
foreach (var routeName in routeNames)
|
foreach (var routeName in routeNames)
|
||||||
{
|
{
|
||||||
var route1 = CreateNamedRoute(routeName);
|
var route1 = CreateNamedRoute(routeName, accept: true);
|
||||||
routes.Add(route1);
|
routes.Add(route1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,8 +560,8 @@ namespace Microsoft.AspNet.Routing
|
||||||
|
|
||||||
private static RouteCollection GetNestedRouteCollection(string[] routeNames)
|
private static RouteCollection GetNestedRouteCollection(string[] routeNames)
|
||||||
{
|
{
|
||||||
var rnd = new Random();
|
var random = new Random();
|
||||||
int index = rnd.Next(0, routeNames.Length - 1);
|
int index = random.Next(0, routeNames.Length - 1);
|
||||||
var first = routeNames.Take(index).ToArray();
|
var first = routeNames.Take(index).ToArray();
|
||||||
var second = routeNames.Skip(index).ToArray();
|
var second = routeNames.Skip(index).ToArray();
|
||||||
|
|
||||||
|
|
@ -279,12 +574,12 @@ namespace Microsoft.AspNet.Routing
|
||||||
rc4.Add(rc2);
|
rc4.Add(rc2);
|
||||||
|
|
||||||
// Add a few unnamedRoutes.
|
// Add a few unnamedRoutes.
|
||||||
rc1.Add(CreateRoute().Object);
|
rc1.Add(CreateRoute(accept: false).Object);
|
||||||
rc2.Add(CreateRoute().Object);
|
rc2.Add(CreateRoute(accept: false).Object);
|
||||||
rc3.Add(CreateRoute().Object);
|
rc3.Add(CreateRoute(accept: false).Object);
|
||||||
rc3.Add(CreateRoute().Object);
|
rc3.Add(CreateRoute(accept: false).Object);
|
||||||
rc4.Add(CreateRoute().Object);
|
rc4.Add(CreateRoute(accept: false).Object);
|
||||||
rc4.Add(CreateRoute().Object);
|
rc4.Add(CreateRoute(accept: false).Object);
|
||||||
|
|
||||||
var routeCollection = new RouteCollection();
|
var routeCollection = new RouteCollection();
|
||||||
routeCollection.Add(rc1);
|
routeCollection.Add(rc1);
|
||||||
|
|
@ -293,13 +588,18 @@ namespace Microsoft.AspNet.Routing
|
||||||
return routeCollection;
|
return routeCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static INamedRouter CreateNamedRoute(string name, bool accept = false)
|
private static INamedRouter CreateNamedRoute(string name, bool accept = false, string matchValue = null)
|
||||||
{
|
{
|
||||||
|
if (matchValue == null)
|
||||||
|
{
|
||||||
|
matchValue = name;
|
||||||
|
}
|
||||||
|
|
||||||
var target = new Mock<INamedRouter>(MockBehavior.Strict);
|
var target = new Mock<INamedRouter>(MockBehavior.Strict);
|
||||||
target
|
target
|
||||||
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
||||||
.Callback<VirtualPathContext>(c => c.IsBound = accept)
|
.Callback<VirtualPathContext>(c => c.IsBound = accept && c.RouteName == name)
|
||||||
.Returns<VirtualPathContext>(c => name)
|
.Returns<VirtualPathContext>(c => c.RouteName == name ? matchValue : null)
|
||||||
.Verifiable();
|
.Verifiable();
|
||||||
|
|
||||||
target
|
target
|
||||||
|
|
@ -315,36 +615,78 @@ namespace Microsoft.AspNet.Routing
|
||||||
return target.Object;
|
return target.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VirtualPathContext CreateVirtualPathContext(string routeName)
|
private static VirtualPathContext CreateVirtualPathContext(
|
||||||
|
string routeName = null,
|
||||||
|
ILoggerFactory loggerFactory = null,
|
||||||
|
RouteOptions options = null)
|
||||||
{
|
{
|
||||||
return new VirtualPathContext(null, null, null, routeName);
|
if (loggerFactory == null)
|
||||||
|
{
|
||||||
|
loggerFactory = NullLoggerFactory.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = new RouteOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var request = new Mock<HttpRequest>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
|
||||||
|
optionsAccessor.SetupGet(o => o.Options).Returns(options);
|
||||||
|
|
||||||
|
var context = new Mock<HttpContext>(MockBehavior.Strict);
|
||||||
|
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
|
||||||
|
.Returns(loggerFactory);
|
||||||
|
context.Setup(m => m.RequestServices.GetService(typeof(IOptions<RouteOptions>)))
|
||||||
|
.Returns(optionsAccessor.Object);
|
||||||
|
context.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
|
||||||
|
return new VirtualPathContext(context.Object, null, null, routeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null)
|
private static RouteContext CreateRouteContext(
|
||||||
|
string requestPath,
|
||||||
|
ILoggerFactory loggerFactory = null,
|
||||||
|
RouteOptions options = null)
|
||||||
{
|
{
|
||||||
if (factory == null)
|
if (loggerFactory == null)
|
||||||
{
|
{
|
||||||
factory = NullLoggerFactory.Instance;
|
loggerFactory = NullLoggerFactory.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = new RouteOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = new Mock<HttpRequest>(MockBehavior.Strict);
|
var request = new Mock<HttpRequest>(MockBehavior.Strict);
|
||||||
request.SetupGet(r => r.Path).Returns(new PathString(requestPath));
|
request.SetupGet(r => r.Path).Returns(new PathString(requestPath));
|
||||||
|
|
||||||
|
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
|
||||||
|
optionsAccessor.SetupGet(o => o.Options).Returns(options);
|
||||||
|
|
||||||
var context = new Mock<HttpContext>(MockBehavior.Strict);
|
var context = new Mock<HttpContext>(MockBehavior.Strict);
|
||||||
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
|
context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
|
||||||
.Returns(factory);
|
.Returns(loggerFactory);
|
||||||
|
context.Setup(m => m.RequestServices.GetService(typeof(IOptions<RouteOptions>)))
|
||||||
|
.Returns(optionsAccessor.Object);
|
||||||
context.SetupGet(c => c.Request).Returns(request.Object);
|
context.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
|
||||||
return new RouteContext(context.Object);
|
return new RouteContext(context.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Mock<IRouter> CreateRoute(bool accept = true)
|
private static Mock<IRouter> CreateRoute(
|
||||||
|
bool accept = true,
|
||||||
|
bool match = false,
|
||||||
|
string matchValue = "value")
|
||||||
{
|
{
|
||||||
var target = new Mock<IRouter>(MockBehavior.Strict);
|
var target = new Mock<IRouter>(MockBehavior.Strict);
|
||||||
target
|
target
|
||||||
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
|
||||||
.Callback<VirtualPathContext>(c => c.IsBound = accept)
|
.Callback<VirtualPathContext>(c => c.IsBound = accept)
|
||||||
.Returns<VirtualPathContext>(null)
|
.Returns(accept || match ? matchValue : null)
|
||||||
.Verifiable();
|
.Verifiable();
|
||||||
|
|
||||||
target
|
target
|
||||||
|
|
|
||||||
|
|
@ -971,7 +971,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetVirtualPath_RejectedByHandler()
|
public void GetVirtualPath_ValuesRejectedByHandler_StillGeneratesPath()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var route = CreateRoute("{controller}", accept: false);
|
var route = CreateRoute("{controller}", accept: false);
|
||||||
|
|
@ -982,7 +982,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(context.IsBound);
|
Assert.False(context.IsBound);
|
||||||
Assert.Null(path);
|
Assert.Equal("Home", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1026,7 +1026,7 @@ namespace Microsoft.AspNet.Routing.Template
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" });
|
var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" });
|
||||||
|
|
||||||
TemplateRoute r = CreateRoute(
|
var r = CreateRoute(
|
||||||
"{p1}/{p2}",
|
"{p1}/{p2}",
|
||||||
new { p2 = "catchall" },
|
new { p2 = "catchall" },
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue