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));