diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
index e9cca0d601..f1a4eaf192 100644
--- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
@@ -203,7 +203,7 @@ namespace Microsoft.AspNet.Routing
}
///
- /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
+ /// In a path segment that contains more than one section, such as a literal section or a parameter, there can only be one optional parameter. The optional parameter must be the last parameter in the segment and must be preceded by one single period (.).
///
internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator
{
@@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Routing
}
///
- /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
+ /// In a path segment that contains more than one section, such as a literal section or a parameter, there can only be one optional parameter. The optional parameter must be the last parameter in the segment and must be preceded by one single period (.).
///
internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator()
{
@@ -347,7 +347,7 @@ namespace Microsoft.AspNet.Routing
}
///
- /// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'.
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
///
internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
{
@@ -355,7 +355,7 @@ namespace Microsoft.AspNet.Routing
}
///
- /// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'.
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
///
internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
{
@@ -363,7 +363,7 @@ namespace Microsoft.AspNet.Routing
}
///
- /// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'.
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
///
internal static string RouteConstraintBuilder_CouldNotResolveConstraint
{
@@ -371,13 +371,29 @@ namespace Microsoft.AspNet.Routing
}
///
- /// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'.
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
///
internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
}
+ ///
+ /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'
+ ///
+ internal static string TemplateRoute_UnescapedBrace
+ {
+ get { return GetString("TemplateRoute_UnescapedBrace"); }
+ }
+
+ ///
+ /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'
+ ///
+ internal static string FormatTemplateRoute_UnescapedBrace()
+ {
+ return GetString("TemplateRoute_UnescapedBrace");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx
index 4b34eea2d3..072dcf21b2 100644
--- a/src/Microsoft.AspNet.Routing/Resources.resx
+++ b/src/Microsoft.AspNet.Routing/Resources.resx
@@ -186,4 +186,7 @@
The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
+
+ In a route parameter, '{' and '}' must be escaped with '{{' and '}}'
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
index 03664f25f5..2c4958b046 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.Text.RegularExpressions;
namespace Microsoft.AspNet.Routing.Template
{
@@ -134,30 +135,41 @@ namespace Microsoft.AspNet.Routing.Template
while (true)
{
- if (context.Current == Separator)
+ if (context.Current == OpenBrace)
{
- // This is a dangling open-brace, which is not allowed
- context.Error = Resources.TemplateRoute_MismatchedParameter;
- return false;
- }
- else if (context.Current == OpenBrace)
- {
- // If we see a '{' while parsing a parameter name it's invalid. We'll just accept it for now
- // and let the validation code for the name find it.
+ // This is an open brace inside of a parameter, it has to be escaped
+ if (context.Next())
+ {
+ if (context.Current != OpenBrace)
+ {
+ // If we see something like "{p1:regex(^\d{3", we will come here.
+ context.Error = Resources.TemplateRoute_UnescapedBrace;
+ return false;
+ }
+ }
+ else
+ {
+ // This is a dangling open-brace, which is not allowed
+ // Example: "{p1:regex(^\d{"
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
}
else if (context.Current == CloseBrace)
{
+ // When we encounter Closed brace here, it either means end of the parameter or it is a closed
+ // brace in the parameter, in that case it needs to be escaped.
+ // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
if (!context.Next())
{
- // This is the end of the string - and we have a valid parameter
+ // This is the end of the string -and we have a valid parameter
context.Back();
break;
}
if (context.Current == CloseBrace)
{
- // This is an 'escaped' brace in a parameter name, which is not allowed.
- // We'll just accept it for now and let the validation code for the name find it.
+ // This is an 'escaped' brace in a parameter name
}
else
{
@@ -176,10 +188,11 @@ namespace Microsoft.AspNet.Routing.Template
}
var rawParameter = context.Capture();
+ var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
// At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters.
- var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter);
+ var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
if (templatePart.IsCatchAll && templatePart.IsOptional)
{
@@ -272,7 +285,7 @@ namespace Microsoft.AspNet.Routing.Template
}
}
- var decoded = encoded.Replace("}}", "}").Replace("{{", "}");
+ var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
if (IsValidLiteral(context, decoded))
{
segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
diff --git a/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs b/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs
index f761e2d838..90da38cdb0 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs
@@ -23,6 +23,9 @@ namespace Microsoft.AspNet.Routing.Tests
[InlineData("Abcd", "abc", true)] // Extra char
[InlineData("^Abcd", "abc", true)] // Extra special char
[InlineData("Abc", " abc", false)] // Missing char
+ [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn
+ [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date
+ [InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email
public void RegexConstraintBuildRegexVerbatimFromInput(string routeValue,
string constraintValue,
bool shouldMatch)
diff --git a/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs
index 3484f10721..c09f45deab 100644
--- a/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs
@@ -49,7 +49,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Arrange, Act & Assert
var ex = Assert.Throws(
() => _constraintResolver.ResolveConstraint("int(5)"));
- Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'"+
+ Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" +
" with the following number of parameters: 1.",
ex.Message);
}
@@ -74,6 +74,17 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.IsType(constraint);
}
+ [Fact]
+ public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint(
+ @"regex(\\b(?\\d{1,2})/(?\\d{1,2})/(?\\d{2,4})\\b)");
+
+ // Assert
+ Assert.IsType(constraint);
+ }
+
[Fact]
public void ResolveConstraint_BoolConstraint()
{
@@ -267,7 +278,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Act & Assert
var ex = Assert.Throws(() => resolver.ResolveConstraint("custom"));
- Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'"+
+ Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" +
" must implement the 'IRouteConstraint' interface.",
ex.Message);
}
@@ -287,6 +298,21 @@ namespace Microsoft.AspNet.Routing.Tests
ex.Message);
}
+ // These are cases which parsing does not catch and we'll end up here
+ [Theory]
+ [InlineData("regex(abc")]
+ [InlineData("int/")]
+ [InlineData("in{t")]
+ public void ResolveConstraint_Invalid_Throws(string constraint)
+ {
+ // Arrange
+ var routeOptions = new RouteOptions();
+ var resolver = GetInlineConstraintResolver(routeOptions);
+
+ // Act & Assert
+ Assert.Null(resolver.ResolveConstraint(constraint));
+ }
+
[Fact]
public void ResolveConstraint_NoMatchingConstructor_Throws()
{
diff --git a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs
index 5d2346727c..c33278d4c1 100644
--- a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs
@@ -256,6 +256,36 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\?)");
}
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn
+
+ // Assert
+ Assert.Equal("p1", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ Assert.Single(templatePart.InlineConstraints);
+ Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)");
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn
+
+ // Assert
+ Assert.Equal("p1", templatePart.Name);
+ Assert.Equal(templatePart.DefaultValue, "123-456-7890");
+ Assert.False(templatePart.IsOptional);
+
+ Assert.Single(templatePart.InlineConstraints);
+ Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)");
+ }
+
[Theory]
[InlineData("", "")]
[InlineData("?", "")]
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs
index 840801bb7d..047f4a44f8 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs
@@ -102,7 +102,26 @@ namespace Microsoft.AspNet.Routing.Template.Tests
}
[Theory]
- [InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "123-456-7890")] // ssn
+ [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "asd@assds.com")] // email
+ [InlineData(@"{p1:regex(([}}])\w+)}", "}sda")] // Not balanced }
+ [InlineData(@"{p1:regex(([{{)])\w+)}", "})sda")] // Not balanced {
+ public void MatchRoute_RegularExpression_Valid(
+ string template,
+ string path)
+ {
+ // Arrange
+ var matcher = CreateMatcher(template);
+
+ // Act
+ var rd = matcher.Match(path);
+
+ // Assert
+ Assert.NotNull(rd);
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
[InlineData("moo/{p1?}", "moo/foo", "foo", null)]
[InlineData("moo/{p1?}", "moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/foo", "foo", null)]
@@ -116,9 +135,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("moo/{p1}.{p2?}", "moo/....", "..", ".")]
[InlineData("moo/{p1}.{p2?}", "moo/.bar", ".bar", null)]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Valid(
- string template,
- string path,
- string p1,
+ string template,
+ string path,
+ string p1,
string p2)
{
// Arrange
@@ -143,13 +162,14 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo", "foo", "moo", null)]
[InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "moo/foo.moo.bar", "foo", "moo", "bar")]
[InlineData("{p1}.{p2?}/{p3}", "foo.moo/bar", "foo", "moo", "bar")]
- [InlineData("{p1}.{p2?}/{p3}", "foo/bar", "foo", null, "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "foo/bar", "foo", null, "bar")]
[InlineData("{p1}.{p2?}/{p3}", ".foo/bar", ".foo", null, "bar")]
+ [InlineData("{p1}/{p2}/{p3?}", "foo/bar/baz", "foo", "bar", "baz")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
- string template,
- string path,
- string p1,
- string p2,
+ string template,
+ string path,
+ string p1,
+ string p2,
string p3)
{
// Arrange
@@ -159,9 +179,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var rd = matcher.Match(path);
// Assert
-
Assert.Equal(p1, rd["p1"]);
-
+
if (p2 != null)
{
Assert.Equal(p2, rd["p2"]);
@@ -173,9 +192,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
}
}
- [Theory]
+ [Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.")]
- [InlineData("moo/{p1}.{p2?}", "moo/.")]
+ [InlineData("moo/{p1}.{p2?}", "moo/.")]
[InlineData("moo/{p1}.{p2}", "foo.")]
[InlineData("moo/{p1}.{p2}", "foo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo.")]
@@ -183,7 +202,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("moo/foo.{p2}.{p3?}", "moo/kungfoo.moo.bar")]
[InlineData("moo/foo.{p2}.{p3?}", "moo/kungfoo.moo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo")]
- [InlineData("{p1}.{p2?}/{p3}", "foo./bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "foo./bar")]
[InlineData("moo/.{p2?}", "moo/.")]
[InlineData("{p1}.{p2}/{p3}", ".foo/bar")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Invalid(string template, string path)
@@ -280,7 +299,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Assert
Assert.Null(rd);
}
-
+
[Fact]
public void NoMatchLongerUrl()
{
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
index 181a586b6f..99da06ac17 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
@@ -4,6 +4,7 @@
#if ASPNET50
using System;
using System.Collections.Generic;
+using System.Linq;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
@@ -417,7 +418,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
defaultValue: null,
inlineConstraints: null));
- expected.Segments.Add(new TemplateSegment());
+ expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
@@ -432,7 +433,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[2]);
-
+
// Act
var actual = TemplateParser.Parse(template);
@@ -462,7 +463,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
- expected.Parameters.Add(expected.Segments[1].Parts[1]);
+ expected.Parameters.Add(expected.Segments[1].Parts[1]);
// Act
var actual = TemplateParser.Parse(template);
@@ -471,7 +472,62 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal(expected, actual, new TemplateEqualityComparer());
}
- [Theory]
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
+ [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
+ [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
+ [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
+ [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
+ public void Parse_RegularExpressions(string template, string constraint)
+ {
+ // Arrange
+ var expected = new RouteTemplate(new List());
+ expected.Segments.Add(new TemplateSegment());
+ var c = new InlineConstraint(constraint);
+ expected.Segments[0].Parts.Add(
+ TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: new List { c }));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
+ [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining
+ [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
+ [InlineData(@"{p1:regex(abc)")]
+ public void Parse_RegularExpressions_Invalid(string template)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws(
+ () => TemplateParser.Parse(template),
+ "There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
+ "'}' character." + Environment.NewLine + "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
+ public void Parse_RegularExpressions_Unescaped(string template)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws(
+ () => TemplateParser.Parse(template),
+ "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'" + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Theory]
[InlineData("{p1?}.{p2?}")]
[InlineData("{p1}.{p2?}.{p3}")]
[InlineData("{p1?}.{p2}/{p3}")]
@@ -485,18 +541,18 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Act and Assert
ExceptionAssert.Throws(
() => TemplateParser.Parse(template),
- "In a path segment that contains more than one section, such as a literal section or a parameter, " +
+ "In a path segment that contains more than one section, such as a literal section or a parameter, " +
"there can only be one optional parameter. The optional parameter must be the last parameter in the " +
"segment and must be preceded by one single period (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}
-
+
[Fact]
public void InvalidTemplate_WithRepeatedParameter()
{
var ex = ExceptionAssert.Throws(
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
- "The route parameter name 'controller' appears more than one time in the route template." +
+ "The route parameter name 'controller' appears more than one time in the route template." +
Environment.NewLine + "Parameter name: routeTemplate");
}
@@ -521,7 +577,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("123{a}abc{*moo}"),
- "A path segment that contains more than one section, such as a literal section or a parameter, " +
+ "A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -531,7 +587,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("{*p1}/{*p2}"),
- "A catch-all parameter can only appear as the last segment of the route template." +
+ "A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -565,7 +621,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("{*a*:int}", "a*")]
[InlineData("{*a*=5}", "a*")]
[InlineData("{*a*b=5}", "a*b")]
- public void ParseRouteParameter_ThrowsIf_ParameterContainsAsterisk(string template, string parameterName)
+ [InlineData("{p1?}.{p2/}/{p3}", "p2/")]
+ [InlineData("{p{{}", "p{")]
+ [InlineData("{p}}}", "p}")]
+ [InlineData("{p/}", "p/")]
+ public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
+ string template,
+ string parameterName)
{
// Arrange
var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " +
@@ -604,7 +666,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("{aaa}/{AAA}"),
- "The route parameter name 'AAA' appears more than one time in the route template." +
+ "The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -614,7 +676,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("{aaa}/{*AAA}"),
- "The route parameter name 'AAA' appears more than one time in the route template." +
+ "The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -624,7 +686,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("{a}/{aa}a}/{z}"),
- "There is an incomplete parameter in the route template. Check that each '{' character has a " +
+ "There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -634,10 +696,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("{a}/{a{aa}/{z}"),
- "The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot" +
- " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
- " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
- " and can occur only at the start of the parameter." + Environment.NewLine +
+ "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'" + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -680,7 +739,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
- "A catch-all parameter can only appear as the last segment of the route template." +
+ "A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -718,7 +777,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws(
() => TemplateParser.Parse("foor?bar"),
- "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
+ "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -741,7 +800,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
ExceptionAssert.Throws(
() => TemplateParser.Parse("{foorb?}-bar-{z}"),
"In a path segment that contains more than one section, such as a literal section or a parameter, " +
- "there can only be one optional parameter. The optional parameter must be the last parameter in " +
+ "there can only be one optional parameter. The optional parameter must be the last parameter in " +
"the segment and must be preceded by one single period (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -814,11 +873,31 @@ namespace Microsoft.AspNet.Routing.Template.Tests
x.IsCatchAll != y.IsCatchAll ||
x.IsOptional != y.IsOptional ||
!String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
- !String.Equals(x.Name, y.Name, StringComparison.Ordinal))
+ !String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
+ (x.InlineConstraints == null && y.InlineConstraints != null) ||
+ (x.InlineConstraints != null && y.InlineConstraints == null))
{
return false;
}
+ if (x.InlineConstraints == null && y.InlineConstraints == null)
+ {
+ return true;
+ }
+
+ if (x.InlineConstraints.Count() != y.InlineConstraints.Count())
+ {
+ return false;
+ }
+
+ foreach (var xconstraint in x.InlineConstraints)
+ {
+ if (!y.InlineConstraints.Any(
+ c => string.Equals(c.Constraint, xconstraint.Constraint)))
+ {
+ return false;
+ }
+ }
return true;
}
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
index 4bbdcf7990..6309dcab39 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs
@@ -12,6 +12,7 @@ using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Routing.Logging;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.Logging;
+using Microsoft.AspNet.WebUtilities;
using Moq;
using Xunit;
@@ -37,6 +38,28 @@ namespace Microsoft.AspNet.Routing.Template
return Tuple.Create(sink, context);
}
+ [Fact]
+ public void CreateTemplate_InlineConstraint_Regex_Malformed()
+ {
+ // Arrange
+ var template = @"{controller}/{action}/ {p1:regex(abc} ";
+ var mockTarget = new Mock(MockBehavior.Strict);
+ var expected = "The constraint entry 'p1' - 'regex(abc' on the route " +
+ "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " +
+ "'IInlineConstraintResolverProxy'.";
+
+ var exception = Assert.Throws(
+ () => new TemplateRoute(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver));
+
+ Assert.Equal(expected, exception.Message);
+ }
+
[Fact]
public async Task RouteAsync_MatchSuccess_LogsCorrectValues()
{
@@ -497,7 +520,7 @@ namespace Microsoft.AspNet.Routing.Template
// Act
await route.RouteAsync(context);
- // Assert
+ // Assert
Assert.True(context.IsHandled);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
@@ -506,6 +529,48 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal("5", context.RouteData.Values["id"]);
}
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_Regex()
+ {
+ // Arrange
+ var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}";
+
+ var context = CreateRouteContext("/Home/Index/123-456-7890");
+
+ 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["ssn"]);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.True(context.IsHandled);
+ Assert.True(routeValues.ContainsKey("ssn"));
+ Assert.Equal("123-456-7890", routeValues["ssn"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("ssn"));
+ Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]);
+ }
+
[Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent()
{
@@ -1782,7 +1847,10 @@ namespace Microsoft.AspNet.Routing.Template
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());
-
+ resolverMock.Setup(o => o.ResolveConstraint(@"regex(^\d{3}-\d{3}-\d{4}$)")).Returns(
+ new RegexInlineRouteConstraint(@"^\d{3}-\d{3}-\d{4}$"));
+ resolverMock.Setup(o => o.ResolveConstraint(@"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")).Returns(
+ new RegexInlineRouteConstraint(@"^\d{1,2}\/\d{1,2}\/\d{4}$"));
return resolverMock.Object;
}
diff --git a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs
index ac3dbea2e9..933e9001d6 100644
--- a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs
@@ -34,6 +34,25 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("12", defaults["id"]);
}
+ [Theory]
+ [InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")]
+ [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")]
+ public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value)
+ {
+ // Arrange & Act
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ template,
+ defaults: null,
+ constraints: null);
+
+ // Assert
+ var defaults = ((Template.TemplateRoute)routeBuilder.Routes[0]).Defaults;
+ Assert.Equal(value, defaults["p1"]);
+ }
+
[Fact]
public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws()
{