зеркало из https://github.com/aspnet/Routing.git
updating OM of routing, about 10% better perf
This commit is contained in:
Родитель
85225055b9
Коммит
179841743e
|
@ -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, ""));
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
namespace Microsoft.AspNet.Routing {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -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 {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_MismatchedParameter {
|
||||
get {
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
<value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
|
||||
<value>There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.</value>
|
||||
<value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
|
||||
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
|
||||
|
|
|
@ -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<TemplateSegment> segments)
|
||||
{
|
||||
if (segments == null)
|
||||
{
|
||||
throw new ArgumentNullException("segments");
|
||||
}
|
||||
|
||||
Segments = segments;
|
||||
}
|
||||
|
||||
public List<TemplateSegment> Segments { get; private set; }
|
||||
|
||||
public IDictionary<string, object> Match(string requestPath, IDictionary<string, object> defaults)
|
||||
{
|
||||
var requestSegments = requestPath.Split(Delimiters);
|
||||
|
||||
if (defaults == null)
|
||||
{
|
||||
defaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var values = new Dictionary<string, object>(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<string, object> defaults, Dictionary<string, object> 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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<PathSubsegment> 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<T>
|
||||
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<PathSubsegment> Subsegments { get; private set; }
|
||||
|
||||
#if ROUTE_DEBUGGING
|
||||
public override string LiteralText
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> s = new List<string>();
|
||||
foreach (PathSubsegment subsegment in Subsegments)
|
||||
{
|
||||
s.Add(subsegment.LiteralText);
|
||||
}
|
||||
return String.Join(String.Empty, s.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
List<string> s = new List<string>();
|
||||
foreach (PathSubsegment subsegment in Subsegments)
|
||||
{
|
||||
s.Add(subsegment.ToString());
|
||||
}
|
||||
return "[ " + String.Join(", ", s.ToArray()) + " ]";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<PathSegment> pathSegments)
|
||||
{
|
||||
Contract.Assert(pathSegments != null);
|
||||
PathSegments = pathSegments;
|
||||
}
|
||||
|
||||
internal IList<PathSegment> PathSegments { get; private set; }
|
||||
|
||||
private static bool ForEachParameter(IList<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> 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<string, object> Match(string virtualPath, IDictionary<string, object> defaultValues)
|
||||
{
|
||||
IList<string> requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath);
|
||||
|
||||
if (defaultValues == null)
|
||||
{
|
||||
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
IDictionary<string, object> matchedValues = new Dictionary<string, object>(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<string> remainingRequestSegments, IDictionary<string, object> defaultValues, IDictionary<string, object> 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<string, object> defaultValues, IDictionary<string, object> 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<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TemplateSegment>();
|
||||
|
||||
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<TemplateSegment> 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<TemplateSegment> 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<string> _parameterNames = new HashSet<string>(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<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template
|
|||
{
|
||||
private readonly IDictionary<string, object> _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<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// The parser will throw for invalid routes.
|
||||
_parsedRoute = TemplateRouteParser.Parse(RouteTemplate);
|
||||
_parsedRoute = TemplateParser.Parse(RouteTemplate);
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Defaults
|
||||
|
|
|
@ -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<string> uriParts = SplitUriToPathSegmentStrings(routeTemplate);
|
||||
Exception ex = ValidateUriParts(uriParts);
|
||||
if (ex != null)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
IList<PathSegment> 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<PathSubsegment> ParseUriSegment(string segment, out Exception exception)
|
||||
{
|
||||
int startIndex = 0;
|
||||
|
||||
List<PathSubsegment> pathSubsegments = new List<PathSubsegment>();
|
||||
|
||||
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<PathSegment> SplitUriToPathSegments(IList<string> uriParts)
|
||||
{
|
||||
List<PathSegment> pathSegments = new List<PathSegment>();
|
||||
|
||||
foreach (string pathSegment in uriParts)
|
||||
{
|
||||
bool isCurrentPartSeparator = IsSeparator(pathSegment);
|
||||
if (isCurrentPartSeparator)
|
||||
{
|
||||
pathSegments.Add(new PathSeparatorSegment());
|
||||
}
|
||||
else
|
||||
{
|
||||
Exception exception;
|
||||
IList<PathSubsegment> 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<string> SplitUriToPathSegmentStrings(string uri)
|
||||
{
|
||||
List<string> parts = new List<string>();
|
||||
|
||||
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<string> pathSegments)
|
||||
{
|
||||
Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null.");
|
||||
|
||||
HashSet<string> usedParameterNames = new HashSet<string>(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<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
||||
if (exception != null)
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
exception = ValidateUriSegment(subsegments, usedParameterNames);
|
||||
if (exception != null)
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
foundCatchAllParameter = subsegments.Any<PathSubsegment>(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<PathSubsegment> pathSubsegments, HashSet<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TemplatePart> _parts = new List<TemplatePart>();
|
||||
|
||||
public List<TemplatePart> Parts
|
||||
{
|
||||
get { return _parts; }
|
||||
}
|
||||
|
||||
internal string DebuggerToString()
|
||||
{
|
||||
return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TemplateSegment>());
|
||||
expected.Segments.Add(new TemplateSegment());
|
||||
expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
|
||||
|
||||
// Act
|
||||
var actual = TemplateParser.Parse(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SingleParameter()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p}";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
expected.Segments.Add(new TemplateSegment());
|
||||
expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false));
|
||||
|
||||
// Act
|
||||
var actual = TemplateParser.Parse(template);
|
||||
|
||||
// Assert
|
||||
Assert.Equal<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleLiterals()
|
||||
{
|
||||
// Arrange
|
||||
var template = "cool/awesome/super";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultipleParameters()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p1}/{p2}/{*p3}";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ComplexSegment_LP()
|
||||
{
|
||||
// Arrange
|
||||
var template = "cool-{p1}";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ComplexSegment_PL()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p1}-cool";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ComplexSegment_PLP()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{p1}-cool-{p2}";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ComplexSegment_LPL()
|
||||
{
|
||||
// Arrange
|
||||
var template = "cool-{p1}-awesome";
|
||||
|
||||
var expected = new ParsedTemplate(new List<TemplateSegment>());
|
||||
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<ParsedTemplate>(expected, actual, new TemplateParsedRouteEqualityComparer());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_WithRepeatedParameter()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ParsedTemplate>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => 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<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foor?bar"),
|
||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<int>(match.Values.Count, expectedValues.Count);
|
||||
Assert.Equal<int>(expectedValues.Count, match.Values.Count);
|
||||
foreach (string key in match.Values.Keys)
|
||||
{
|
||||
Assert.Equal(expectedValues[key], match.Values[key]);
|
||||
|
|
Загрузка…
Ссылка в новой задаче