diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..8cdac84de3
--- /dev/null
+++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs
@@ -0,0 +1,115 @@
+//
+namespace Microsoft.AspNet.Routing
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
+ ///
+ internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment
+ {
+ get { return GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); }
+ }
+
+ ///
+ /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
+ ///
+ internal static string TemplateRoute_CannotHaveConsecutiveParameters
+ {
+ get { return GetString("TemplateRoute_CannotHaveConsecutiveParameters"); }
+ }
+
+ ///
+ /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
+ ///
+ internal static string TemplateRoute_CannotHaveConsecutiveSeparators
+ {
+ get { return GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); }
+ }
+
+ ///
+ /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
+ ///
+ internal static string TemplateRoute_CannotHaveOptionalParameterInMultiSegment
+ {
+ get { return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment"); }
+ }
+
+ ///
+ /// A catch-all parameter can only appear as the last segment of the route template.
+ ///
+ internal static string TemplateRoute_CatchAllMustBeLast
+ {
+ get { return GetString("TemplateRoute_CatchAllMustBeLast"); }
+ }
+
+ ///
+ /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
+ ///
+ internal static string TemplateRoute_InvalidLiteral
+ {
+ get { return GetString("TemplateRoute_InvalidLiteral"); }
+ }
+
+ ///
+ /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can only occur at the end of the parameter.
+ ///
+ internal static string TemplateRoute_InvalidParameterName
+ {
+ get { return GetString("TemplateRoute_InvalidParameterName"); }
+ }
+
+ ///
+ /// The route template cannot start with a '/' or '~' character.
+ ///
+ internal static string TemplateRoute_InvalidRouteTemplate
+ {
+ get { return GetString("TemplateRoute_InvalidRouteTemplate"); }
+ }
+
+ ///
+ /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
+ ///
+ internal static string TemplateRoute_MismatchedParameter
+ {
+ get { return GetString("TemplateRoute_MismatchedParameter"); }
+ }
+
+ ///
+ /// The route parameter name '{0}' appears more than one time in the route template.
+ ///
+ internal static string TemplateRoute_RepeatedParameter
+ {
+ get { return GetString("TemplateRoute_RepeatedParameter"); }
+ }
+
+ ///
+ /// The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.
+ ///
+ internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint
+ {
+ get { return GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"); }
+ }
+
+ private static string GetString(string name, params string[] argumentNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ for (var i = 0; i < argumentNames.Length; i++)
+ {
+ value = value.Replace("{" + argumentNames[i] + "}", "{" + i + "}");
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs
deleted file mode 100644
index 34805f5e59..0000000000
--- a/src/Microsoft.AspNet.Routing/Resources.Designer.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.34003
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace Microsoft.AspNet.Routing {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resources {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resources() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
-#if NET45
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly);
-#else
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
-#endif
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- ///
- /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter..
- ///
- internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment {
- get {
- return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string..
- ///
- internal static string TemplateRoute_CannotHaveConsecutiveParameters {
- get {
- return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value..
- ///
- internal static string TemplateRoute_CannotHaveConsecutiveSeparators {
- get {
- return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template..
- ///
- internal static string TemplateRoute_CatchAllMustBeLast {
- get {
- return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?".
- ///
- internal static string TemplateRoute_InvalidParameterName {
- get {
- return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character..
- ///
- internal static string TemplateRoute_InvalidRouteTemplate {
- get {
- return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character..
- ///
- internal static string TemplateRoute_MismatchedParameter {
- get {
- return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template..
- ///
- internal static string TemplateRoute_RepeatedParameter {
- get {
- return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'..
- ///
- internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint {
- get {
- return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture);
- }
- }
- }
-}
diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx
index 5372daea5b..35e303768d 100644
--- a/src/Microsoft.AspNet.Routing/Resources.resx
+++ b/src/Microsoft.AspNet.Routing/Resources.resx
@@ -126,14 +126,20 @@
The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
+
+ A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter.
+
A catch-all parameter can only appear as the last segment of the route template.
+
+ The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
+
- The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?"
+ The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can only occur at the end of the parameter.
- The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.
+ The route template cannot start with a '/' or '~' character.
There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs
index 743c8465e8..94a32d38c9 100644
--- a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs
+++ b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs
@@ -144,6 +144,10 @@ namespace Microsoft.AspNet.Routing.Template
{
values.Add(part.Name, defaultValue);
}
+ else if (part.IsOptional)
+ {
+ // This is optional (with no default value) - there's nothing to capture here, so just move on.
+ }
else
{
// There's no default for this (non-catch-all) parameter so it can't match.
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
index 21c9a87f69..3b87faeecb 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs
@@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Routing.Template
private const char Separator = '/';
private const char OpenBrace = '{';
private const char CloseBrace = '}';
+ private const char EqualsSign = '=';
+ private const char QuestionMark = '?';
public static ParsedTemplate Parse(string routeTemplate)
{
@@ -174,10 +176,15 @@ namespace Microsoft.AspNet.Routing.Template
var rawName = context.Capture();
var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal);
- var parameterName = isCatchAll ? rawName.Substring(1) : rawName;
+ var isOptional = rawName.EndsWith("?", StringComparison.Ordinal);
+
+ rawName = isCatchAll ? rawName.Substring(1) : rawName;
+ rawName = isOptional ? rawName.Substring(0, rawName.Length - 1) : rawName;
+
+ var parameterName = rawName;
if (IsValidParameterName(context, parameterName))
{
- segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll));
+ segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll, isOptional));
return true;
}
else
@@ -250,8 +257,15 @@ namespace Microsoft.AspNet.Routing.Template
}
var decoded = encoded.Replace("}}", "}").Replace("{{", "}");
- segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
- return true;
+ if (IsValidLiteral(context, decoded))
+ {
+ segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
}
private static bool IsAllValid(TemplateParserContext context, List segments)
@@ -287,6 +301,17 @@ namespace Microsoft.AspNet.Routing.Template
}
}
+ // if a segment has multiple parts, then the parameters can't be optional
+ for (int i = 0; i < segment.Parts.Count; i++)
+ {
+ var part = segment.Parts[i];
+ if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1)
+ {
+ context.Error = Resources.TemplateRoute_CannotHaveOptionalParameterInMultiSegment;
+ return false;
+ }
+ }
+
// A segment cannot containt two consecutive parameters
var isLastSegmentParameter = false;
for (int i = 0; i < segment.Parts.Count; i++)
@@ -315,7 +340,7 @@ namespace Microsoft.AspNet.Routing.Template
for (int i = 0; i < parameterName.Length; i++)
{
var c = parameterName[i];
- if (c == '/' || c == '{' || c == '}')
+ if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark)
{
context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName);
return false;
@@ -331,11 +356,24 @@ namespace Microsoft.AspNet.Routing.Template
return true;
}
+ private static bool IsValidLiteral(TemplateParserContext context, string literal)
+ {
+ Contract.Assert(context != null);
+ Contract.Assert(literal != null);
+
+ if (literal.IndexOf(QuestionMark) != -1)
+ {
+ context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidLiteral, literal);
+ return false;
+ }
+
+ return true;
+ }
+
private static bool IsInvalidRouteTemplate(string routeTemplate)
{
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
- routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
- (routeTemplate.IndexOf('?') != -1);
+ routeTemplate.StartsWith("/", StringComparison.Ordinal);
}
diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
index e21306d4ad..1a361f5399 100644
--- a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
+++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs
@@ -16,20 +16,21 @@ namespace Microsoft.AspNet.Routing.Template
};
}
- public static TemplatePart CreateParameter(string name, bool isCatchAll)
+ public static TemplatePart CreateParameter(string name, bool isCatchAll, bool isOptional)
{
return new TemplatePart()
{
IsParameter = true,
Name = name,
IsCatchAll = isCatchAll,
+ IsOptional = isOptional,
};
}
public bool IsCatchAll { get; private set; }
public bool IsLiteral { get; private set; }
public bool IsParameter { get; private set; }
-
+ public bool IsOptional { get; private set; }
public string Name { get; private set; }
public string Text { get; private set; }
@@ -37,7 +38,7 @@ namespace Microsoft.AspNet.Routing.Template
{
if (IsParameter)
{
- return "{" + (IsCatchAll ? "*" : string.Empty) + Name + "}";
+ return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}";
}
else
{
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
index 0b08688037..1f3ca9d78b 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs
@@ -35,7 +35,24 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false));
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_OptionalParameter()
+ {
+ // Arrange
+ var template = "{p?}";
+
+ var expected = new ParsedTemplate(new List());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true));
// Act
var actual = TemplateParser.Parse(template);
@@ -73,11 +90,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false));
expected.Segments.Add(new TemplateSegment());
- expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false));
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false, false));
expected.Segments.Add(new TemplateSegment());
- expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true));
+ expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true, false));
// Act
var actual = TemplateParser.Parse(template);
@@ -95,7 +112,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false));
// Act
var actual = TemplateParser.Parse(template);
@@ -112,7 +129,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
// Act
@@ -130,9 +147,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false, false));
// Act
var actual = TemplateParser.Parse(template);
@@ -150,7 +167,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var expected = new ParsedTemplate(new List());
expected.Segments.Add(new TemplateSegment());
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
- expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false));
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
// Act
@@ -215,7 +232,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("foo/{*}"),
- @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
+ "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -269,7 +287,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.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: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
+ "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 only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -278,7 +297,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("{a}/{}/{z}"),
- @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
+ "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -287,7 +307,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("{Controller}.mvc/{?}"),
- "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
+ "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -323,7 +344,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("/foo"),
- "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
+ "The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -332,7 +353,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("~foo"),
- "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
+ "The route template cannot start with a '/' or '~' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@@ -341,10 +362,29 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
Assert.Throws(
() => TemplateParser.Parse("foor?bar"),
- "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
+ "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
+ [Fact]
+ public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
+ {
+ Assert.Throws(
+ () => TemplateParser.Parse("{foor?b}"),
+ "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " +
+ "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_MultiSegmentParameterCannotContainOptionalParameter()
+ {
+ Assert.Throws(
+ () => TemplateParser.Parse("{foorb?}-bar-{z}"),
+ "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
private class TemplateParsedRouteEqualityComparer : IEqualityComparer
{
public bool Equals(ParsedTemplate x, ParsedTemplate y)
@@ -379,6 +419,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
if (xPart.IsLiteral != yPart.IsLiteral ||
xPart.IsParameter != yPart.IsParameter ||
xPart.IsCatchAll != yPart.IsCatchAll ||
+ xPart.IsOptional != yPart.IsOptional ||
!String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal) ||
!String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal))
{
diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs
index 4c67210827..8d80336a3c 100644
--- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs
+++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs
@@ -710,6 +710,52 @@ namespace Microsoft.AspNet.Routing.Template.Tests
new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } });
}
+ [Fact]
+ public void MatchSetsOptionalParameter()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action?}");
+ var url = "Home/Index";
+
+ // Act
+ var match = route.Match(new RouteContext(GetHttpContext(url)));
+
+ // Assert
+ Assert.NotNull(match);
+ Assert.Equal("Index", match.Values["action"]);
+ }
+
+ [Fact]
+ public void MatchDoesNotSetOptionalParameter()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action?}");
+ var url = "Home";
+
+ // Act
+ var match = route.Match(new RouteContext(GetHttpContext(url)));
+
+ // Assert
+ Assert.NotNull(match);
+ Assert.False(match.Values.ContainsKey("action"));
+ }
+
+ [Fact]
+ public void MatchMultipleOptionalParameters()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action?}/{id?}");
+ var url = "Home/Index";
+
+ // Act
+ var match = route.Match(new RouteContext(GetHttpContext(url)));
+
+ // Assert
+ Assert.NotNull(match);
+ Assert.Equal("Index", match.Values["action"]);
+ Assert.False(match.Values.ContainsKey("id"));
+ }
+
private static IRouteValues CreateRouteData()
{
return new RouteValues(new Dictionary(StringComparer.OrdinalIgnoreCase));