diff --git a/src/Microsoft.AspNet.Routing/Constraints/OptionalRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Constraints/OptionalRouteConstraint.cs
new file mode 100644
index 0000000000..2890505e81
--- /dev/null
+++ b/src/Microsoft.AspNet.Routing/Constraints/OptionalRouteConstraint.cs
@@ -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
+{
+ ///
+ /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint.
+ ///
+ 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 values,
+ RouteDirection routeDirection)
+ {
+ object value;
+ if (values.TryGetValue(routeKey, out value))
+ {
+ return InnerConstraint.Match(httpContext,
+ route,
+ routeKey,
+ values,
+ routeDirection);
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs
index 59f8efbe2d..bb03d0a62e 100644
--- a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs
+++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs
@@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Routing
private readonly string _displayName;
private readonly Dictionary> _constraints;
-
+ private readonly HashSet _optionalParameters;
///
/// Creates a new instance.
///
@@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Routing
_displayName = displayName;
_constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ _optionalParameters = new HashSet(StringComparer.OrdinalIgnoreCase);
}
///
@@ -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);
}
+ ///
+ /// Sets the given key as optional.
+ ///
+ /// The key.
+ public void SetOptional([NotNull] string key)
+ {
+ _optionalParameters.Add(key);
+ }
+
private void Add(string key, IRouteConstraint constraint)
{
List list;
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs
index 9af02c11a4..05d041e401 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs
@@ -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();
diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs
index 472fb238a9..e01ad7cefc 100644
--- a/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/RouteConstraintBuilderTest.cs
@@ -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(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(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(Assert.Single(result).Value);
+ var optionalConstraint = (OptionalRouteConstraint)result.First().Value;
+ var compositeConstraint = Assert.IsType(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
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
index 13b2a860d1..5baaf3b9c8 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
@@ -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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(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(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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(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(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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.IsHandled = true;
+ })
+ .Returns(Task.FromResult(true));
+
+ var constraints = new Dictionary();
+ 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(route.Constraints["id"]);
+ var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint;
+ Assert.IsType(innerConstraint);
+ var compositeConstraint = (CompositeRouteConstraint)innerConstraint;
+ Assert.Equal(compositeConstraint.Constraints.Count(), 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 routeValues = null;
+ var mockTarget = new Mock(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny()))
+ .Callback(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(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();
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;
}