From 179841743e2d3f8fcf5209165d08d5b3c3bf58db Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 7 Feb 2014 15:09:01 -0800 Subject: [PATCH] updating OM of routing, about 10% better perf --- samples/RoutingSample/Startup.cs | 3 +- .../Resources.Designer.cs | 5 +- src/Microsoft.AspNet.Routing/Resources.resx | 2 +- .../Template/ParsedTemplate.cs | 301 +++++++++++++ .../Template/PathContentSegment.cs | 65 --- .../Template/PathLiteralSubsegment.cs | 30 -- .../Template/PathParameterSubsegment.cs | 42 -- .../Template/PathSegment.cs | 15 - .../Template/PathSeparatorSegment.cs | 23 - .../Template/PathSubsegment.cs | 15 - .../Template/TemplateParsedRoute.cs | 381 ----------------- .../Template/TemplateParser.cs | 404 ++++++++++++++++++ .../Template/TemplatePart.cs | 48 +++ .../Template/TemplateRoute.cs | 4 +- .../Template/TemplateRouteParser.cs | 373 ---------------- .../Template/TemplateSegment.cs | 26 ++ .../Template/TemplateParserTests.cs | 400 +++++++++++++++++ .../Template/TemplateRouteParserTests.cs | 198 --------- .../Template/TemplateRouteTests.cs | 11 +- 19 files changed, 1192 insertions(+), 1154 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplatePart.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs delete mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index 6c87a888b9..d169128183 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing.Owin; +using Microsoft.AspNet.Routing.Template; using Owin; namespace RoutingSample @@ -25,7 +26,7 @@ namespace RoutingSample var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); routes.Add(new PrefixRoute(endpoint1, "api/store")); - routes.Add(new PrefixRoute(endpoint1, "api/checkout")); + routes.Add(new TemplateRoute(endpoint1, "api/checkout/{*extra}")); routes.Add(new PrefixRoute(endpoint2, "hello/world")); routes.Add(new PrefixRoute(endpoint1, "")); } diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs index 87554390e5..44df4b6429 100644 --- a/src/Microsoft.AspNet.Routing/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Resources.Designer.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNet.Routing { using System; - using System.Reflection; /// @@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Routing { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -116,7 +115,7 @@ namespace Microsoft.AspNet.Routing { } /// - /// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.. + /// 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 { diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 76c3adf227..5372daea5b 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -136,7 +136,7 @@ The route template cannot start with a '/' or '~' character and it cannot contain a '?' character. - There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character. + There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. The route parameter name '{0}' appears more than one time in the route template. diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs new file mode 100644 index 0000000000..19a92fedc0 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class ParsedTemplate + { + private const string SeparatorString = "/"; + private const char SeparatorChar = '/'; + + private static readonly char[] Delimiters = new char[] { SeparatorChar }; + + public ParsedTemplate(List segments) + { + if (segments == null) + { + throw new ArgumentNullException("segments"); + } + + Segments = segments; + } + + public List Segments { get; private set; } + + public IDictionary Match(string requestPath, IDictionary defaults) + { + var requestSegments = requestPath.Split(Delimiters); + + if (defaults == null) + { + defaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < requestSegments.Length; i++) + { + var routeSegment = Segments.Count > i ? Segments[i] : null; + var requestSegment = requestSegments[i]; + + if (routeSegment == null) + { + // If pathSegment is null, then we're out of route segments. All we can match is the empty + // string. + if (requestSegment.Length > 0) + { + return null; + } + } + else if (routeSegment.Parts.Count == 1) + { + // Optimize for the simple case - the segment is made up for a single part + var part = routeSegment.Parts[0]; + if (part.IsLiteral) + { + if (!part.Text.Equals(requestSegment, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + } + else + { + Contract.Assert(part.IsParameter); + + if (part.IsCatchAll) + { + var captured = string.Join(SeparatorString, requestSegments, i, requestSegments.Length - i); + if (captured.Length > 0) + { + values.Add(part.Name, captured); + } + else + { + // It's ok for a catch-all to produce a null value + object defaultValue; + defaults.TryGetValue(part.Name, out defaultValue); + + values.Add(part.Name, defaultValue); + } + + // A catch-all has to be the last part, so we're done. + break; + } + else + { + if (requestSegment.Length > 0) + { + values.Add(part.Name, requestSegment); + } + else + { + object defaultValue; + if (defaults.TryGetValue(part.Name, out defaultValue)) + { + values.Add(part.Name, defaultValue); + } + else + { + // There's no default for this parameter + return null; + } + } + } + } + } + else + { + if (!MatchComplexSegment(routeSegment, requestSegment, defaults, values)) + { + return null; + } + } + } + + for (int i = requestSegments.Length; i < Segments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. These need + // to be all single-part parameter segments with default values or else they won't match. + var routeSegment = Segments[i]; + if (routeSegment.Parts.Count > 1) + { + // If it has more than one part it must contain literals, so it can't match. + return null; + } + + + var part = routeSegment.Parts[0]; + if (part.IsLiteral) + { + return null; + } + + Contract.Assert(part.IsParameter); + + // It's ok for a catch-all to produce a null value + object defaultValue; + if (defaults.TryGetValue(part.Name, out defaultValue) || part.IsCatchAll) + { + values.Add(part.Name, defaultValue); + } + else + { + // There's no default for this (non-catch-all) parameter so it can't match. + return null; + } + } + + // Copy all remaining default values to the route data + if (defaults != null) + { + foreach (var kvp in defaults) + { + if (!values.ContainsKey(kvp.Key)) + { + values.Add(kvp.Key, kvp.Value); + } + } + } + + return values; + } + + private bool MatchComplexSegment(TemplateSegment routeSegment, string requestSegment, IDictionary defaults, Dictionary values) + { + Contract.Assert(routeSegment != null); + Contract.Assert(routeSegment.Parts.Count > 1); + + // Find last literal segment and get its last index in the string + int lastIndex = requestSegment.Length; + int indexOfLastSegmentUsed = routeSegment.Parts.Count - 1; + + TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered + + while (indexOfLastSegmentUsed >= 0) + { + int newLastIndex = lastIndex; + + var part = routeSegment.Parts[indexOfLastSegmentUsed]; + if (part.IsParameter) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = part; + } + else + { + Contract.Assert(part.IsLiteral); + lastLiteral = part; + + int startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + int indexOfLiteral = requestSegment.LastIndexOf(part.Text, startIndex, StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) + { + if ((indexOfLiteral + part.Text.Length) != requestSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + + if ((parameterNeedsValue != null) && (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Text.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + string parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength); + + if (string.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + values.Add(parameterNeedsValue.Name, parameterValueString); + } + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + return (lastIndex == 0) || routeSegment.Parts[0].IsParameter; + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs deleted file mode 100644 index e0382db613..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. - internal sealed class PathContentSegment : PathSegment - { - public PathContentSegment(IList subsegments) - { - Subsegments = subsegments; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] - public bool IsCatchAll - { - get - { - // TODO: Verify this is correct. Maybe add an assert. - // Performance sensitive - // Caching count is faster for IList - int subsegmentCount = Subsegments.Count; - for (int i = 0; i < subsegmentCount; i++) - { - PathSubsegment seg = Subsegments[i]; - PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment; - if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) - { - return true; - } - } - return false; - } - } - - public IList Subsegments { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.LiteralText); - } - return String.Join(String.Empty, s.ToArray()); - } - } - - public override string ToString() - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.ToString()); - } - return "[ " + String.Join(", ", s.ToArray()) + " ]"; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs deleted file mode 100644 index 312597a387..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a literal subsegment of a ContentPathSegment - internal sealed class PathLiteralSubsegment : PathSubsegment - { - public PathLiteralSubsegment(string literal) - { - Literal = literal; - } - - public string Literal { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return Literal; - } - } - - public override string ToString() - { - return "\"" + Literal + "\""; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs deleted file mode 100644 index 2edf14ccef..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a parameter subsegment of a ContentPathSegment - internal sealed class PathParameterSubsegment : PathSubsegment - { - public PathParameterSubsegment(string parameterName) - { - if (parameterName.StartsWith("*", StringComparison.Ordinal)) - { - ParameterName = parameterName.Substring(1); - IsCatchAll = true; - } - else - { - ParameterName = parameterName; - } - } - - public bool IsCatchAll { get; private set; } - - public string ParameterName { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } - } - - public override string ToString() - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSegment.cs deleted file mode 100644 index f58279ef0f..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSegment.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a segment of a URI such as a separator or content - public abstract class PathSegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs deleted file mode 100644 index 88cb876f70..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a "/" separator in a URI - internal sealed class PathSeparatorSegment : PathSegment - { -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "/"; - } - } - - public override string ToString() - { - return "\"/\""; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs deleted file mode 100644 index 60e0175f50..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. - internal abstract class PathSubsegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs deleted file mode 100644 index 49db3415a7..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Microsoft.AspNet.Routing.Template -{ - public class TemplateParsedRoute - { - public TemplateParsedRoute(IList pathSegments) - { - Contract.Assert(pathSegments != null); - PathSegments = pathSegments; - } - - internal IList PathSegments { get; private set; } - - private static bool ForEachParameter(IList pathSegments, Func action) - { - for (int i = 0; i < pathSegments.Count; i++) - { - PathSegment pathSegment = pathSegments[i]; - - if (pathSegment is PathSeparatorSegment) - { - // We only care about parameter subsegments, so skip this - continue; - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) - { - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // We only care about parameter subsegments, so skip this - continue; - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - if (!action(parameterSubsegment)) - { - return false; - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - return true; - } - - public IDictionary Match(string virtualPath, IDictionary defaultValues) - { - IList requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath); - - if (defaultValues == null) - { - defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - IDictionary matchedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // This flag gets set once all the data in the URI has been parsed through, but - // the route we're trying to match against still has more parts. At this point - // we'll only continue matching separator characters and parameters that have - // default values. - bool ranOutOfStuffToParse = false; - - // This value gets set once we start processing a catchall parameter (if there is one - // at all). Once we set this value we consume all remaining parts of the URI into its - // parameter value. - bool usedCatchAllParameter = false; - - for (int i = 0; i < PathSegments.Count; i++) - { - PathSegment pathSegment = PathSegments[i]; - - if (requestPathSegments.Count <= i) - { - ranOutOfStuffToParse = true; - } - - string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; - - if (pathSegment is PathSeparatorSegment) - { - if (ranOutOfStuffToParse) - { - // If we're trying to match a separator in the route but there's no more content, that's OK - } - else - { - if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal)) - { - return null; - } - } - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - if (contentPathSegment.IsCatchAll) - { - Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment."); - MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); - usedCatchAllParameter = true; - } - else - { - if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) - { - return null; - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - if (!usedCatchAllParameter) - { - if (PathSegments.Count < requestPathSegments.Count) - { - // If we've already gone through all the parts defined in the route but the URI - // still contains more content, check that the remaining content is all separators. - for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) - { - if (!TemplateRouteParser.IsSeparator(requestPathSegments[i])) - { - return null; - } - } - } - } - - // Copy all remaining default values to the route data - if (defaultValues != null) - { - foreach (var defaultValue in defaultValues) - { - if (!matchedValues.ContainsKey(defaultValue.Key)) - { - matchedValues.Add(defaultValue.Key, defaultValue.Value); - } - } - } - - return matchedValues; - } - - private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, IDictionary defaultValues, IDictionary matchedValues) - { - string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray()); - - PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; - - object catchAllValue; - - if (remainingRequest.Length > 0) - { - catchAllValue = remainingRequest; - } - else - { - defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); - } - - matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); - } - - private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary defaultValues, IDictionary matchedValues) - { - if (String.IsNullOrEmpty(requestPathSegment)) - { - // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match - - if (routeSegment.Subsegments.Count > 1) - { - return false; - } - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; - if (parameterSubsegment == null) - { - return false; - } - - // We must have a default value since there's no value in the request URI - object parameterValue; - if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) - { - // If there's a default value for this parameter, use that default value - matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); - return true; - } - else - { - // If there's no default value, this segment doesn't match - return false; - } - } - - // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal - if (routeSegment.Subsegments.Count == 1) - { - return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); - } - - // Find last literal segment and get its last index in the string - - int lastIndex = requestPathSegment.Length; - int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; - - PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered - - while (indexOfLastSegmentUsed >= 0) - { - int newLastIndex = lastIndex; - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; - if (parameterSubsegment != null) - { - // Hold on to the parameter so that we can fill it in when we locate the next literal - parameterNeedsValue = parameterSubsegment; - } - else - { - PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; - if (literalSubsegment != null) - { - lastLiteral = literalSubsegment; - - int startIndex = lastIndex - 1; - // If we have a pending parameter subsegment, we must leave at least one character for that - if (parameterNeedsValue != null) - { - startIndex--; - } - - if (startIndex < 0) - { - return false; - } - - int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); - if (indexOfLiteral == -1) - { - // If we couldn't find this literal index, this segment cannot match - return false; - } - - // If the first subsegment is a literal, it must match at the right-most extent of the request URI. - // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". - // This check is related to the check we do at the very end of this function. - if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) - { - if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) - { - return false; - } - } - - newLastIndex = indexOfLiteral; - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - - if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) - { - // If we have a pending parameter that needs a value, grab that value - - int parameterStartIndex; - int parameterTextLength; - - if (lastLiteral == null) - { - if (indexOfLastSegmentUsed == 0) - { - parameterStartIndex = 0; - } - else - { - parameterStartIndex = newLastIndex; - Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); - } - parameterTextLength = lastIndex; - } - else - { - // If we're getting a value for a parameter that is somewhere in the middle of the segment - if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) - { - parameterStartIndex = 0; - parameterTextLength = lastIndex; - } - else - { - parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; - parameterTextLength = lastIndex - parameterStartIndex; - } - } - - string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); - - if (String.IsNullOrEmpty(parameterValueString)) - { - // If we're here that means we have a segment that contains multiple sub-segments. - // For these segments all parameters must have non-empty values. If the parameter - // has an empty value it's not a match. - return false; - } - else - { - // If there's a value in the segment for this parameter, use the subsegment value - matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); - } - - parameterNeedsValue = null; - lastLiteral = null; - } - - lastIndex = newLastIndex; - indexOfLastSegmentUsed--; - } - - // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of - // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment - // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching - // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* - // request URI in order for it to be a match. - // This check is related to the check we do earlier in this function for LiteralSubsegments. - return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment); - } - - private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary matchedValues) - { - PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment; - if (parameterSubsegment == null) - { - // Handle a single literal segment - PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment; - Contract.Assert(literalSubsegment != null, "Invalid path segment type"); - return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); - } - else - { - // Handle a single parameter segment - matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); - return true; - } - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs new file mode 100644 index 0000000000..21c9a87f69 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + public static class TemplateParser + { + private const char Separator = '/'; + private const char OpenBrace = '{'; + private const char CloseBrace = '}'; + + public static ParsedTemplate Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = String.Empty; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); + } + + var context = new TemplateParserContext(routeTemplate); + var segments = new List(); + + while (context.Next()) + { + if (context.Current == Separator) + { + // If we get here is means that there's a consecutive '/' character. Templates don't start with a '/' and + // parsing a segment consumes the separator. + throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate"); + } + else + { + if (!ParseSegment(context, segments)) + { + throw new ArgumentException(context.Error, "routeTemplate"); + } + } + } + + if (IsAllValid(context, segments)) + { + return new ParsedTemplate(segments); + } + else + { + throw new ArgumentException(context.Error, "routeTemplate"); + } + } + + private static bool ParseSegment(TemplateParserContext context, List segments) + { + Contract.Assert(context != null); + Contract.Assert(segments != null); + + var segment = new TemplateSegment(); + + while (true) + { + if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" + context.Back(); + if (!ParseLiteral(context, segment)) + { + return false; + } + } + else + { + // This is the inside of a parameter + if (!ParseParameter(context, segment)) + { + return false; + } + } + } + else if (context.Current == Separator) + { + // We've reached the end of the segment + break; + } + else + { + if (!ParseLiteral(context, segment)) + { + return false; + } + } + + if (!context.Next()) + { + // We've reached the end of the string + break; + } + } + + if (IsSegmentValid(context, segment)) + { + segments.Add(segment); + return true; + } + else + { + return false; + } + } + + private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + while (true) + { + if (context.Current == Separator) + { + // 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. + } + else if (context.Current == CloseBrace) + { + if (!context.Next()) + { + // 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. + } + else + { + // This is the end of the parameter + context.Back(); + break; + } + } + + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + var rawName = context.Capture(); + + var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); + var parameterName = isCatchAll ? rawName.Substring(1) : rawName; + if (IsValidParameterName(context, parameterName)) + { + segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll)); + return true; + } + else + { + return false; + } + } + + private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + string encoded; + while (true) + { + if (context.Current == Separator) + { + encoded = context.Capture(); + context.Back(); + break; + } + else if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // We've just seen the start of a parameter, so back up and return + context.Back(); + encoded = context.Capture(); + context.Back(); + break; + } + } + else if (context.Current == CloseBrace) + { + if (!context.Next()) + { + // This is a dangling close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // This is an unbalanced close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + if (!context.Next()) + { + encoded = context.Capture(); + break; + } + } + + var decoded = encoded.Replace("}}", "}").Replace("{{", "}"); + segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); + return true; + } + + private static bool IsAllValid(TemplateParserContext context, List segments) + { + // A catch-all parameter must be the last part of the last segment + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + for (int j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter && part.IsCatchAll && (i != segments.Count - 1 || j != segment.Parts.Count - 1)) + { + context.Error = Resources.TemplateRoute_CatchAllMustBeLast; + return false; + } + } + } + + return true; + } + + private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment) + { + // If a segment has multiple parts, then it can't contain a catch all. + for (int i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1) + { + context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; + return false; + } + } + + // A segment cannot containt two consecutive parameters + var isLastSegmentParameter = false; + for (int i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && isLastSegmentParameter) + { + context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; + return false; + } + + isLastSegmentParameter = part.IsParameter; + } + + return true; + } + + private static bool IsValidParameterName(TemplateParserContext context, string parameterName) + { + if (parameterName.Length == 0) + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + + for (int i = 0; i < parameterName.Length; i++) + { + var c = parameterName[i]; + if (c == '/' || c == '{' || c == '}') + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + } + + if (!context.ParameterNames.Add(parameterName)) + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName); + return false; + } + + return true; + } + + private static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || + routeTemplate.StartsWith("/", StringComparison.Ordinal) || + (routeTemplate.IndexOf('?') != -1); + } + + + private class TemplateParserContext + { + private readonly string _template; + private int _index; + private int? _mark; + + private HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + public TemplateParserContext(string template) + { + Contract.Assert(template != null); + _template = template; + + _index = -1; + } + + public char Current + { + get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } + } + + public string Error + { + get; + set; + } + + public HashSet ParameterNames + { + get { return _parameterNames; } + } + + public bool Back() + { + return --_index >= 0; + } + + public bool Next() + { + return ++_index < _template.Length; + } + + public void Mark() + { + _mark = _index; + } + + public string Capture() + { + if (_mark.HasValue) + { + var value = _template.Substring(_mark.Value, _index - _mark.Value); + _mark = null; + return value; + } + else + { + return null; + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs new file mode 100644 index 0000000000..e21306d4ad --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplatePart + { + public static TemplatePart CreateLiteral(string text) + { + return new TemplatePart() + { + IsLiteral = true, + Text = text, + }; + } + + public static TemplatePart CreateParameter(string name, bool isCatchAll) + { + return new TemplatePart() + { + IsParameter = true, + Name = name, + IsCatchAll = isCatchAll, + }; + } + + public bool IsCatchAll { get; private set; } + public bool IsLiteral { get; private set; } + public bool IsParameter { get; private set; } + + public string Name { get; private set; } + public string Text { get; private set; } + + internal string DebuggerToString() + { + if (IsParameter) + { + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + "}"; + } + else + { + return Text; + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 76f4a68b66..4eab348596 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template { private readonly IDictionary _defaults; private readonly IRouteEndpoint _endpoint; - private readonly TemplateParsedRoute _parsedRoute; + private readonly ParsedTemplate _parsedRoute; private readonly string _routeTemplate; public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate) @@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Routing.Template _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); // The parser will throw for invalid routes. - _parsedRoute = TemplateRouteParser.Parse(RouteTemplate); + _parsedRoute = TemplateParser.Parse(RouteTemplate); } public IDictionary Defaults diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs deleted file mode 100644 index d82a7b3ec6..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.Linq; - -namespace Microsoft.AspNet.Routing.Template -{ - public static class TemplateRouteParser - { - private static string GetLiteral(string segmentLiteral) - { - // Scan for errant single { and } and convert double {{ to { and double }} to } - - // First we eliminate all escaped braces and then check if any other braces are remaining - string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty); - if (newLiteral.Contains("{") || newLiteral.Contains("}")) - { - return null; - } - - // If it's a valid format, we unescape the braces - return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); - } - - private static int IndexOfFirstOpenParameter(string segment, int startIndex) - { - // Find the first unescaped open brace - while (true) - { - startIndex = segment.IndexOf('{', startIndex); - if (startIndex == -1) - { - // If there are no more open braces, stop - return -1; - } - if ((startIndex + 1 == segment.Length) || - ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) - { - // If we found an open brace that is followed by a non-open brace, it's - // a parameter delimiter. - // It's also a delimiter if the open brace is the last character - though - // it ends up being being called out as invalid later on. - return startIndex; - } - // Increment by two since we want to skip both the open brace that - // we're on as well as the subsequent character since we know for - // sure that it is part of an escape sequence. - startIndex += 2; - } - } - - internal static bool IsSeparator(string s) - { - return String.Equals(s, "/", StringComparison.Ordinal); - } - - private static bool IsValidParameterName(string parameterName) - { - if (parameterName.Length == 0) - { - return false; - } - - for (int i = 0; i < parameterName.Length; i++) - { - char c = parameterName[i]; - if (c == '/' || c == '{' || c == '}') - { - return false; - } - } - - return true; - } - - internal static bool IsInvalidRouteTemplate(string routeTemplate) - { - return routeTemplate.StartsWith("~", StringComparison.Ordinal) || - routeTemplate.StartsWith("/", StringComparison.Ordinal) || - (routeTemplate.IndexOf('?') != -1); - } - - public static TemplateParsedRoute Parse(string routeTemplate) - { - if (routeTemplate == null) - { - routeTemplate = String.Empty; - } - - if (IsInvalidRouteTemplate(routeTemplate)) - { - throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); - } - - IList uriParts = SplitUriToPathSegmentStrings(routeTemplate); - Exception ex = ValidateUriParts(uriParts); - if (ex != null) - { - throw ex; - } - - IList pathSegments = SplitUriToPathSegments(uriParts); - - Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); - - return new TemplateParsedRoute(pathSegments); - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static IList ParseUriSegment(string segment, out Exception exception) - { - int startIndex = 0; - - List pathSubsegments = new List(); - - while (startIndex < segment.Length) - { - int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); - if (nextParameterStart == -1) - { - // If there are no more parameters in the segment, capture the remainder as a literal and stop - string lastLiteralPart = GetLiteral(segment.Substring(startIndex)); - if (lastLiteralPart == null) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - - return null; - } - - if (lastLiteralPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); - } - break; - } - - int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); - if (nextParameterEnd == -1) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - return null; - } - - string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); - if (literalPart == null) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - return null; - } - - if (literalPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); - } - - string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); - pathSubsegments.Add(new PathParameterSubsegment(parameterName)); - - startIndex = nextParameterEnd + 1; - } - - exception = null; - return pathSubsegments; - } - - private static IList SplitUriToPathSegments(IList uriParts) - { - List pathSegments = new List(); - - foreach (string pathSegment in uriParts) - { - bool isCurrentPartSeparator = IsSeparator(pathSegment); - if (isCurrentPartSeparator) - { - pathSegments.Add(new PathSeparatorSegment()); - } - else - { - Exception exception; - IList subsegments = ParseUriSegment(pathSegment, out exception); - Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); - pathSegments.Add(new PathContentSegment(subsegments)); - } - } - return pathSegments; - } - - internal static IList SplitUriToPathSegmentStrings(string uri) - { - List parts = new List(); - - if (String.IsNullOrEmpty(uri)) - { - return parts; - } - - int currentIndex = 0; - - // Split the incoming URI into individual parts - while (currentIndex < uri.Length) - { - int indexOfNextSeparator = uri.IndexOf('/', currentIndex); - if (indexOfNextSeparator == -1) - { - // If there are no more separators, the rest of the string is the last part - string finalPart = uri.Substring(currentIndex); - if (finalPart.Length > 0) - { - parts.Add(finalPart); - } - break; - } - - string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); - if (nextPart.Length > 0) - { - parts.Add(nextPart); - } - - Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); - parts.Add("/"); - currentIndex = indexOfNextSeparator + 1; - } - - return parts; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriParts(IList pathSegments) - { - Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); - - HashSet usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); - bool? isPreviousPartSeparator = null; - - bool foundCatchAllParameter = false; - - foreach (string pathSegment in pathSegments) - { - if (foundCatchAllParameter) - { - // If we ever start an iteration of the loop and we've already found a - // catchall parameter then we have an invalid URI format. - return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate"); - } - - bool isCurrentPartSeparator; - if (isPreviousPartSeparator == null) - { - // Prime the loop with the first value - isPreviousPartSeparator = IsSeparator(pathSegment); - isCurrentPartSeparator = isPreviousPartSeparator.Value; - } - else - { - isCurrentPartSeparator = IsSeparator(pathSegment); - - // If both the previous part and the current part are separators, it's invalid - if (isCurrentPartSeparator && isPreviousPartSeparator.Value) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate"); - } - - Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); - isPreviousPartSeparator = isCurrentPartSeparator; - } - - // If it's not a separator, parse the segment for parameters and validate it - if (!isCurrentPartSeparator) - { - Exception exception; - IList subsegments = ParseUriSegment(pathSegment, out exception); - if (exception != null) - { - return exception; - } - - exception = ValidateUriSegment(subsegments, usedParameterNames); - if (exception != null) - { - return exception; - } - - foundCatchAllParameter = subsegments.Any(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll); - } - } - return null; - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriSegment(IList pathSubsegments, HashSet usedParameterNames) - { - bool segmentContainsCatchAll = false; - - Type previousSegmentType = null; - - foreach (PathSubsegment subsegment in pathSubsegments) - { - if (previousSegmentType != null) - { - if (previousSegmentType == subsegment.GetType()) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate"); - } - } - previousSegmentType = subsegment.GetType(); - - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // Nothing to validate for literals - everything is valid - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - string parameterName = parameterSubsegment.ParameterName; - - if (parameterSubsegment.IsCatchAll) - { - segmentContainsCatchAll = true; - } - - // Check for valid characters in the parameter name - if (!IsValidParameterName(parameterName)) - { - return new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName), - "routeTemplate"); - } - - if (usedParameterNames.Contains(parameterName)) - { - return new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName), - "routeTemplate"); - } - else - { - usedParameterNames.Add(parameterName); - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - - if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate"); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs b/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs new file mode 100644 index 0000000000..12b1ecf136 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplateSegment + { + private readonly List _parts = new List(); + + public List Parts + { + get { return _parts; } + } + + internal string DebuggerToString() + { + return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs new file mode 100644 index 0000000000..0b08688037 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateRouteParserTests + { + [Fact] + public void Parse_SingleLiteral() + { + // Arrange + var template = "cool"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false)); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + 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)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + 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.CreateLiteral("cool-")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + 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.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + 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.CreateLiteral("-awesome")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = Assert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + Assert.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"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateParser.Parse("123{a}abc{*moo}"), + "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"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + Assert.Throws( + () => TemplateParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateParser.Parse("{*p1}abc{*p2}"), + "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"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + 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 + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + 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 + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + 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 + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{?}"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + Assert.Throws( + () => TemplateParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithSlash() + { + Assert.Throws( + () => TemplateParser.Parse("/foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + Assert.Throws( + () => TemplateParser.Parse("~foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + Assert.Throws( + () => TemplateParser.Parse("foor?bar"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + private class TemplateParsedRouteEqualityComparer : IEqualityComparer + { + public bool Equals(ParsedTemplate x, ParsedTemplate y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else + { + if (x.Segments.Count != y.Segments.Count) + { + return false; + } + + for (int i = 0; i < x.Segments.Count; i++) + { + if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) + { + return false; + } + + for (int j = 0; j < x.Segments[i].Parts.Count; j++) + { + var xPart = x.Segments[i].Parts[j]; + var yPart = y.Segments[i].Parts[j]; + + if (xPart.IsLiteral != yPart.IsLiteral || + xPart.IsParameter != yPart.IsParameter || + xPart.IsCatchAll != yPart.IsCatchAll || + !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal) || + !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal)) + { + return false; + } + } + } + + return true; + } + } + + public int GetHashCode(ParsedTemplate obj) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs deleted file mode 100644 index 5096687d93..0000000000 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; -using Xunit.Extensions; - -namespace Microsoft.AspNet.Routing.Template.Tests -{ - public class TemplateRouteParserTests - { - [Fact] - public void InvalidTemplate_WithRepeatedParameter() - { - var ex = Assert.Throws( - () => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"), - "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); - } - - [Theory] - [InlineData("123{a}abc{")] - [InlineData("123{a}abc}")] - [InlineData("xyz}123{a}abc}")] - [InlineData("{{p1}")] - [InlineData("{p1}}")] - [InlineData("p1}}p2{")] - public void InvalidTemplate_WithMismatchedBraces(string template) - { - Assert.Throws( - () => TemplateRouteParser.Parse(template), - @"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() - { - Assert.Throws( - () => TemplateRouteParser.Parse("123{a}abc{*moo}"), - "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"); - } - - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{*p1}/{*p2}"), - "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{*p1}abc{*p2}"), - "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"); - } - - [Fact] - public void InvalidTemplate_CannotHaveCatchAllWithNoName() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{*}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{{p1}"), - "There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{p1}}"), - "There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_SameParameterTwiceThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{aaa}/{AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{aaa}/{*AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"), - "There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() - { - Assert.Throws( - () => TemplateRouteParser.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 + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}/{}/{z}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{Controller}.mvc/{?}"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}//{z}"), - "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"), - "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_RepeatedParametersThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/aa{p1}{p2}"), - "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotStartWithSlash() - { - Assert.Throws( - () => TemplateRouteParser.Parse("/foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotStartWithTilde() - { - Assert.Throws( - () => TemplateRouteParser.Parse("~foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotContainQuestionMark() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foor?bar"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - } -} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 5c52254d1a..4c67210827 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -20,12 +20,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); // Act - var rd = r.Match(new RouteContext(context)); + var match = r.Match(new RouteContext(context)); // Assert - Assert.Equal("Bank", rd.Values["controller"]); - Assert.Equal("DoAction", rd.Values["action"]); - Assert.Equal("123", rd.Values["id"]); + Assert.NotNull(match); + Assert.Equal("Bank", match.Values["controller"]); + Assert.Equal("DoAction", match.Values["action"]); + Assert.Equal("123", match.Values["id"]); } [Fact] @@ -736,7 +737,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests else { Assert.NotNull(match); - Assert.Equal(match.Values.Count, expectedValues.Count); + Assert.Equal(expectedValues.Count, match.Values.Count); foreach (string key in match.Values.Keys) { Assert.Equal(expectedValues[key], match.Values[key]);