Fixing the issue #123.

Added OptionalRouteConstraint class to take care of optional inline parameter. It will create the OptionalRouteConstraint for a inline parameter that is optional with real constraint on the parameter as inner constraint of OptionalRouteConstraint.
This commit is contained in:
Mugdha Kulkarni 2014-11-17 16:46:38 -08:00 committed by Mugdha Kulkarni
parent 0e3e53dcfe
commit f549a550a9
5 changed files with 475 additions and 3 deletions

View File

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Routing.Constraints
{
/// <summary>
/// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint.
/// </summary>
public class OptionalRouteConstraint : IRouteConstraint
{
public OptionalRouteConstraint([NotNull] IRouteConstraint innerConstraint)
{
InnerConstraint = innerConstraint;
}
public IRouteConstraint InnerConstraint { get; }
public bool Match([NotNull] HttpContext httpContext,
[NotNull] IRouter route,
[NotNull] string routeKey,
[NotNull] IDictionary<string, object> values,
RouteDirection routeDirection)
{
object value;
if (values.TryGetValue(routeKey, out value))
{
return InnerConstraint.Match(httpContext,
route,
routeKey,
values,
routeDirection);
}
return true;
}
}
}

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Routing
private readonly string _displayName;
private readonly Dictionary<string, List<IRouteConstraint>> _constraints;
private readonly HashSet<string> _optionalParameters;
/// <summary>
/// Creates a new <see cref="RouteConstraintBuilder"/> instance.
/// </summary>
@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Routing
_displayName = displayName;
_constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
_optionalParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@ -55,7 +56,15 @@ namespace Microsoft.AspNet.Routing
constraint = new CompositeRouteConstraint(kvp.Value.ToArray());
}
constraints.Add(kvp.Key, constraint);
if (_optionalParameters.Contains(kvp.Key))
{
var optionalConstraint = new OptionalRouteConstraint(constraint);
constraints.Add(kvp.Key, optionalConstraint);
}
else
{
constraints.Add(kvp.Key, constraint);
}
}
return constraints;
@ -123,6 +132,15 @@ namespace Microsoft.AspNet.Routing
Add(key, constraint);
}
/// <summary>
/// Sets the given key as optional.
/// </summary>
/// <param name="key">The key.</param>
public void SetOptional([NotNull] string key)
{
_optionalParameters.Add(key);
}
private void Add(string key, IRouteConstraint constraint)
{
List<IRouteConstraint> list;

View File

@ -274,10 +274,15 @@ namespace Microsoft.AspNet.Routing.Template
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
}
return constraintBuilder.Build();

View File

@ -102,6 +102,53 @@ namespace Microsoft.AspNet.Routing
"of type 'DefaultInlineConstraintResolver'.");
}
[Fact]
public void AddResolvedConstraint_ForOptionalParameter()
{
var builder = CreateBuilder("{controller}/{action}/{id}");
builder.SetOptional("id");
builder.AddResolvedConstraint("id", "int");
var result = builder.Build();
Assert.Equal(1, result.Count);
Assert.Equal("id", result.First().Key);
Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
}
[Fact]
public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter()
{
var builder = CreateBuilder("{controller}/{action}/{id}");
builder.AddResolvedConstraint("id", "int");
builder.SetOptional("id");
var result = builder.Build();
Assert.Equal(1, result.Count);
Assert.Equal("id", result.First().Key);
Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
}
[Fact]
public void AddResolvedConstraint_And_AddConstraint_ForOptionalParameter()
{
var builder = CreateBuilder("{controller}/{action}/{name}");
builder.SetOptional("name");
builder.AddResolvedConstraint("name", "alpha");
var minLenConstraint = new MinLengthRouteConstraint(10);
builder.AddConstraint("name", minLenConstraint);
var result = builder.Build();
Assert.Equal(1, result.Count);
Assert.Equal("name", result.First().Key);
Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
var optionalConstraint = (OptionalRouteConstraint)result.First().Value;
var compositeConstraint = Assert.IsType<CompositeRouteConstraint>(optionalConstraint.InnerConstraint); ;
Assert.Equal(compositeConstraint.Constraints.Count(), 2);
Assert.Single(compositeConstraint.Constraints, c => c is MinLengthRouteConstraint);
Assert.Single(compositeConstraint.Constraints, c => c is AlphaRouteConstraint);
}
[Theory]
[InlineData("abc", "abc", true)] // simple case
[InlineData("abc", "bbb|abc", true)] // Regex or

View File

@ -224,6 +224,82 @@ namespace Microsoft.AspNet.Routing.Template
Assert.NotSame(route.DataTokens, context.RouteData.DataTokens);
}
[Fact]
public async Task RouteAsync_InlineConstrait_OptionalParameter()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index/5");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
// Assert
Assert.NotNull(routeValues);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
Assert.True(context.RouteData.Values.ContainsKey("id"));
Assert.Equal("5", context.RouteData.Values["id"]);
}
[Fact]
public async Task RouteAsync_InlineConstrait_OptionalParameter_NotPresent()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
// Assert
Assert.NotNull(routeValues);
Assert.False(routeValues.ContainsKey("id"));
Assert.False(context.RouteData.Values.ContainsKey("id"));
}
[Fact]
public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint()
{
@ -463,6 +539,177 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal(0, sink.Writes.Count);
}
[Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index/5");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
Assert.NotEmpty(route.Constraints);
Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
Assert.True(context.RouteData.Values.ContainsKey("id"));
Assert.Equal("5", context.RouteData.Values["id"]);
}
[Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
Assert.NotEmpty(route.Constraints);
Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.NotNull(routeValues);
Assert.False(routeValues.ContainsKey("id"));
Assert.False(context.RouteData.Values.ContainsKey("id"));
}
[Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter_WithInConstructorConstraint()
{
// Arrange
var template = "{controller}/{action}/{id:int?}";
var context = CreateRouteContext("/Home/Index/5");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var constraints = new Dictionary<string, object>();
constraints.Add("id", new RangeRouteConstraint(1, 20));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: constraints,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
Assert.NotEmpty(route.Constraints);
Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint;
Assert.IsType<CompositeRouteConstraint>(innerConstraint);
var compositeConstraint = (CompositeRouteConstraint)innerConstraint;
Assert.Equal(compositeConstraint.Constraints.Count<IRouteConstraint>(), 2);
Assert.Single(compositeConstraint.Constraints, c => c is IntRouteConstraint);
Assert.Single(compositeConstraint.Constraints, c => c is RangeRouteConstraint);
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
Assert.True(context.RouteData.Values.ContainsKey("id"));
Assert.Equal("5", context.RouteData.Values["id"]);
}
[Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter_ConstraintFails()
{
// Arrange
var template = "{controller}/{action}/{id:range(1,20)?}";
var context = CreateRouteContext("/Home/Index/100");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
Assert.NotEmpty(route.Constraints);
Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
// Act
await route.RouteAsync(context);
// Assert
Assert.False(context.IsHandled);
}
#region Route Matching
// PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases.
@ -994,6 +1241,118 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key));
}
[Fact]
public void GetVirtualPath_InlineConstraints_Success()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", id = 4 });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/4", path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_NonMatchingvalue()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", id = "asf" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Null(path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int?}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", id = 98 });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/98", path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_OptionalParameter_ValueNotPresent()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int?}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index", path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int?}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", id = "sdfd" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Null(path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_CompositeInlineConstraint()
{
// Arrange
var route = CreateRoute("{controller}/{action}/{id:int:range(1,20)}");
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", id = 14 });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/14", path);
}
[Fact]
public void GetVirtualPath_InlineConstraints_CompositeConstraint_FromConstructor()
{
// Arrange
var constraint = new MaxLengthRouteConstraint(20);
var route = CreateRoute(
template: "{controller}/{action}/{name:alpha}",
defaults: null,
accept: true,
constraints: new { name = constraint });
var context = CreateVirtualPathContext(
values: new { action = "Index", controller = "Home", name = "products" });
// Act
var path = route.GetVirtualPath(context);
// Assert
Assert.Equal("Home/Index/products", path);
}
private static VirtualPathContext CreateVirtualPathContext(object values)
{
return CreateVirtualPathContext(new RouteValueDictionary(values), null);
@ -1263,6 +1622,9 @@ namespace Microsoft.AspNet.Routing.Template
{
var resolverMock = new Mock<IInlineConstraintResolver>();
resolverMock.Setup(o => o.ResolveConstraint("int")).Returns(new IntRouteConstraint());
resolverMock.Setup(o => o.ResolveConstraint("range(1,20)")).Returns(new RangeRouteConstraint(1, 20));
resolverMock.Setup(o => o.ResolveConstraint("alpha")).Returns(new AlphaRouteConstraint());
return resolverMock.Object;
}