From d4904e870174cfe31f0799741299efa18f228c8b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 6 Feb 2014 11:26:25 -0800 Subject: [PATCH] Adding legacy rounting code - mostly unmodified to get it into history --- .../Resources.Designer.cs | 145 + src/Microsoft.AspNet.Routing/Resources.resx | 147 + .../Template/BoundRouteTemplate.cs | 16 + .../Template/ITemplateRouteConstraint.cs | 12 + .../Template/IVirtualPathData.cs | 11 + .../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/RouteDirection.cs | 10 + .../Template/TemplateParsedRoute.cs | 842 ++++++ .../Template/TemplateRoute.cs | 229 ++ .../Template/TemplateRouteParser.cs | 373 +++ .../Template/VirtualPathData.cs | 42 + .../Template/Assert.cs | 25 + .../Template/TemplateRouteParserTests.cs | 198 ++ .../Template/TemplateRouteTests.cs | 2595 +++++++++++++++++ .../project.json | 5 +- 20 files changed, 4839 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Routing/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Routing/Resources.resx create mode 100644 src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/RouteDirection.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs new file mode 100644 index 0000000000..87554390e5 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Resources.Designer.cs @@ -0,0 +1,145 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Routing { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.. + /// + internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.. + /// + internal static string TemplateRoute_CannotHaveConsecutiveParameters { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.. + /// + internal static string TemplateRoute_CannotHaveConsecutiveSeparators { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template.. + /// + internal static string TemplateRoute_CatchAllMustBeLast { + get { + return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?". + /// + internal static string TemplateRoute_InvalidParameterName { + get { + return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.. + /// + internal static string TemplateRoute_InvalidRouteTemplate { + get { + return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.. + /// + internal static string TemplateRoute_MismatchedParameter { + get { + return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template.. + /// + internal static string TemplateRoute_RepeatedParameter { + get { + return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.. + /// + internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint { + get { + return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx new file mode 100644 index 0000000000..76c3adf227 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + + + The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + + A catch-all parameter can only appear as the last segment of the route template. + + + The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?" + + + 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. + + + The route parameter name '{0}' appears more than one time in the route template. + + + The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs new file mode 100644 index 0000000000..8d73a43dec --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Template +{ + /// + /// Represents a URI generated from a . + /// + public class BoundRouteTemplate + { + public string BoundTemplate { get; set; } + + public IDictionary Values { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs new file mode 100644 index 0000000000..4f70d7306b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing.Template +{ + public interface ITemplateRouteConstraint + { + bool Match(HttpContext context, IRoute route, string parameterName, IDictionary values, RouteDirection routeDirection); + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs new file mode 100644 index 0000000000..f7f2cafd19 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public interface IVirtualPathData + { + IRoute Route { get; } + + string VirtualPath { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs new file mode 100644 index 0000000000..e0382db613 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs @@ -0,0 +1,65 @@ +// 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 new file mode 100644 index 0000000000..312597a387 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs @@ -0,0 +1,30 @@ +// 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 new file mode 100644 index 0000000000..2edf14ccef --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 0000000000..f58279ef0f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSegment.cs @@ -0,0 +1,15 @@ +// 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 new file mode 100644 index 0000000000..88cb876f70 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs @@ -0,0 +1,23 @@ +// 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 new file mode 100644 index 0000000000..60e0175f50 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs @@ -0,0 +1,15 @@ +// 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/RouteDirection.cs b/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs new file mode 100644 index 0000000000..7f9bcc6c33 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public enum RouteDirection + { + UriResolution = 0, + UriGeneration + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs new file mode 100644 index 0000000000..9b65710400 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs @@ -0,0 +1,842 @@ +// 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; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNet.Routing.Template +{ + public sealed class TemplateParsedRoute + { + public TemplateParsedRoute(IList pathSegments) + { + Contract.Assert(pathSegments != null); + PathSegments = pathSegments; + } + + internal IList PathSegments { get; private set; } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] + public BoundRouteTemplate Bind(IDictionary currentValues, IDictionary values, IDictionary defaultValues, IDictionary constraints) + { + if (currentValues == null) + { + currentValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (values == null) + { + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (defaultValues == null) + { + defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // The set of values we should be using when generating the URI in this route + IDictionary acceptedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Keep track of which new values have been used + HashSet unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + // If it's a parameter subsegment, examine the current value to see if it matches the new value + string parameterName = parameterSubsegment.ParameterName; + + object newParameterValue; + bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + unusedNewValues.Remove(parameterName); + } + + object currentParameterValue; + bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + return false; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + acceptedValues.Add(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + acceptedValues.Add(parameterName, currentParameterValue); + } + } + return true; + }); + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var newValue in values) + { + if (IsRoutePartNonEmpty(newValue.Value)) + { + if (!acceptedValues.ContainsKey(newValue.Key)) + { + acceptedValues.Add(newValue.Key, newValue.Value); + } + } + } + + // Add all current values that aren't in the URI at all + foreach (var currentValue in currentValues) + { + string parameterName = currentValue.Key; + if (!acceptedValues.ContainsKey(parameterName)) + { + PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); + if (parameterSubsegment == null) + { + acceptedValues.Add(parameterName, currentValue.Value); + } + } + } + + // Add all remaining default values from the route to the list of values we will use for URI generation + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + object defaultValue; + if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue); + } + } + return true; + }); + + // All required parameters in this URI must have values from somewhere (i.e. the accepted values) + bool hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + object defaultValue; + if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + // If the route parameter value is required that means there's + // no default value, so if there wasn't a new value for it + // either, this route won't match. + return false; + } + } + return true; + }); + if (!hasAllRequiredValues) + { + return null; + } + + // All other default values must match if they are explicitly defined in the new values + IDictionary otherDefaultValues = new Dictionary(defaultValues, StringComparer.OrdinalIgnoreCase); + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + otherDefaultValues.Remove(parameterSubsegment.ParameterName); + return true; + }); + + foreach (var defaultValue in otherDefaultValues) + { + object value; + if (values.TryGetValue(defaultValue.Key, out value)) + { + unusedNewValues.Remove(defaultValue.Key); + if (!RoutePartsEqual(value, defaultValue.Value)) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + + // Step 2: If the route is a match generate the appropriate URI + + StringBuilder uri = new StringBuilder(); + StringBuilder pendingParts = new StringBuilder(); + + bool pendingPartsAreAllSafe = false; + bool blockAllUriAppends = false; + + for (int i = 0; i < PathSegments.Count; i++) + { + PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart + + if (pathSegment is PathSeparatorSegment) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + pendingPartsAreAllSafe = false; + + // Guard against appending multiple separators for empty segments + if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') + { + // Dev10 676725: Route should not be matched if that causes mismatched tokens + // Dev11 86819: We will allow empty matches if all subsequent segments are null + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI (without the trailing slash) and prevent any future appends + uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); + pendingParts.Length = 0; + blockAllUriAppends = true; + } + else + { + pendingParts.Append("/"); + } + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + bool addedAnySubsegments = false; + + foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) + { + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // If it's a literal we hold on to it until we are sure we need to add it + pendingPartsAreAllSafe = true; + pendingParts.Append(literalSubsegment.Literal); + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + + addedAnySubsegments = true; + } + } + pendingPartsAreAllSafe = false; + + // If it's a parameter, get its value + object acceptedParameterValue; + bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); + if (hasAcceptedParameterValue) + { + unusedNewValues.Remove(parameterSubsegment.ParameterName); + } + + object defaultParameterValue; + defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); + + if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) + { + // If the accepted value is the same as the default value, mark it as pending since + // we won't necessarily add it to the URI we generate. + pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + } + else + { + if (blockAllUriAppends) + { + return null; + } + + // Add the new part to the URI as well as any pending parts + if (pendingParts.Length > 0) + { + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + + addedAnySubsegments = true; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (addedAnySubsegments) + { + // See comment above about why we add the pending parts + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + } + } + + // Process constraints keys + if (constraints != null) + { + // If there are any constraints, mark all the keys as being used so that we don't + // generate query string items for custom constraints that don't appear as parameters + // in the URI format. + foreach (var constraintsItem in constraints) + { + unusedNewValues.Remove(constraintsItem.Key); + } + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + StringBuilder encodedUri = new StringBuilder(); + encodedUri.Append(UriEncode(uri.ToString())); + uri = encodedUri; + + // Add remaining new values as query string parameters to the URI + if (unusedNewValues.Count > 0) + { + // Generate the query string + bool firstParam = true; + foreach (string unusedNewValue in unusedNewValues) + { + object value; + if (acceptedValues.TryGetValue(unusedNewValue, out value)) + { + uri.Append(firstParam ? '?' : '&'); + firstParam = false; + uri.Append(Uri.EscapeDataString(unusedNewValue)); + uri.Append('='); + uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); + } + } + } + + return new BoundRouteTemplate + { + BoundTemplate = uri.ToString(), + Values = acceptedValues + }; + } + + private static string EscapeReservedCharacters(Match m) + { + return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture); + } + + 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; + } + + private static PathParameterSubsegment GetParameterSubsegment(IList pathSegments, string parameterName) + { + PathParameterSubsegment foundParameterSubsegment = null; + + ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase)) + { + foundParameterSubsegment = parameterSubsegment; + return false; + } + else + { + return true; + } + }); + + return foundParameterSubsegment; + } + + private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary defaultValues, out object defaultValue) + { + if (parameterSubsegment.IsCatchAll) + { + defaultValue = null; + return false; + } + + return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + string routePartString = routePart as string; + if (routePartString != null) + { + return routePartString.Length > 0; + } + return routePart != null; + } + + 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; + } + } + + private static bool RoutePartsEqual(object a, object b) + { + string sa = a as string; + string sb = b as string; + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs new file mode 100644 index 0000000000..c0ab8dcb44 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -0,0 +1,229 @@ +// 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.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing.Template +{ + /// + /// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the + /// same as the System.Web.Routing.Route implementation. + /// This class has the same URL matching functionality as System.Web.Routing.Route. However, + /// in order for this route to match when generating URLs, a special "httproute" key must be + /// specified when generating the URL. + /// + public class TemplateRoute : IRoute + { + /// + /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API). + /// If this key is not specified then no HTTP routes will match. + /// + public static readonly string HttpRouteKey = "httproute"; + + private string _routeTemplate; + private IDictionary _defaults; + private IDictionary _constraints; + private IDictionary _dataTokens; + + public TemplateRoute() + : this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate) + : this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults) + : this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints) + : this(routeTemplate, defaults, constraints, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens) + : this(routeTemplate, defaults, constraints, dataTokens, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IRouteEndpoint handler) + { + _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; + _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _constraints = constraints ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _dataTokens = dataTokens ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + Handler = handler; + + // The parser will throw for invalid routes. + ParsedRoute = TemplateRouteParser.Parse(RouteTemplate); + } + + public IDictionary Defaults + { + get { return _defaults; } + } + + public IDictionary Constraints + { + get { return _constraints; } + } + + public IDictionary DataTokens + { + get { return _dataTokens; } + } + + public IRouteEndpoint Handler { get; private set; } + + public string RouteTemplate + { + get { return _routeTemplate; } + } + + internal TemplateParsedRoute ParsedRoute { get; private set; } + + public virtual RouteMatch GetRouteData(HttpContext request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + var requestPath = request.Request.Path.Value; + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/') + { + requestPath = requestPath.Substring(1); + } + + IDictionary values = ParsedRoute.Match(requestPath, _defaults); + if (values == null) + { + // If we got back a null value set, that means the URI did not match + return null; + } + + // Validate the values + if (!ProcessConstraints(request, values, RouteDirection.UriResolution)) + { + return null; + } + + return new RouteMatch(null, values); + } + + /// + /// Attempt to generate a URI that represents the values passed in based on current + /// values from the and new values using the specified . + /// + /// The HTTP request message. + /// The route values. + /// A instance or null if URI cannot be generated. + public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary values) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + // Only perform URL generation if the "httproute" key was specified. This allows these + // routes to be ignored when a regular MVC app tries to generate URLs. Without this special + // key an HTTP route used for Web API would normally take over almost all the routes in a + // typical app. + if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase)) + { + return null; + } + // Remove the value from the collection so that it doesn't affect the generated URL + var newValues = GetRouteDictionaryWithoutHttpRouteKey(values); + + IRouteValues routeData = request.GetFeature(); + IDictionary requestValues = routeData == null ? null : routeData.Values; + + BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints); + if (result == null) + { + return null; + } + + // Assert that the route matches the validation rules + if (!ProcessConstraints(request, result.Values, RouteDirection.UriGeneration)) + { + return null; + } + + return new VirtualPathData(this, result.BoundTemplate); + } + + private static IDictionary GetRouteDictionaryWithoutHttpRouteKey(IDictionary routeValues) + { + var newRouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (routeValues != null) + { + foreach (var routeValue in routeValues) + { + if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase)) + { + newRouteValues.Add(routeValue.Key, routeValue.Value); + } + } + } + return newRouteValues; + } + + protected virtual bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + ITemplateRouteConstraint customConstraint = constraint as ITemplateRouteConstraint; + if (customConstraint != null) + { + return customConstraint.Match(request, this, parameterName, values, routeDirection); + } + + // If there was no custom constraint, then treat the constraint as a string which represents a Regex. + string constraintsRule = constraint as string; + if (constraintsRule == null) + { + throw new InvalidOperationException(String.Format( + CultureInfo.CurrentCulture, + Resources.TemplateRoute_ValidationMustBeStringOrCustomConstraint, + parameterName, + RouteTemplate, + typeof(ITemplateRouteConstraint).Name)); + } + + object parameterValue; + values.TryGetValue(parameterName, out parameterValue); + string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture); + string constraintsRegEx = "^(" + constraintsRule + ")$"; + return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + + private bool ProcessConstraints(HttpContext request, IDictionary values, RouteDirection routeDirection) + { + if (Constraints != null) + { + foreach (KeyValuePair constraintsItem in Constraints) + { + if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) + { + return false; + } + } + } + + return true; + } + + public RouteMatch Match(RouteContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs new file mode 100644 index 0000000000..d82a7b3ec6 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs @@ -0,0 +1,373 @@ +// 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/VirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs new file mode 100644 index 0000000000..e6ec047dcd --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs @@ -0,0 +1,42 @@ +// 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 +{ + public class VirtualPathData : IVirtualPathData + { + private string _virtualPath; + + public VirtualPathData(IRoute route, string virtualPath) + { + if (route == null) + { + throw new ArgumentNullException("route"); + } + + if (virtualPath == null) + { + throw new ArgumentNullException("virtualPath"); + } + + Route = route; + VirtualPath = virtualPath; + } + + public IRoute Route { get; private set; } + + public string VirtualPath + { + get { return _virtualPath; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _virtualPath = value; + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs new file mode 100644 index 0000000000..927b39a679 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class Assert : Xunit.Assert + { + public static T Throws(Assert.ThrowsDelegate action, string message) where T : Exception + { + T exception = Assert.Throws(action); + Assert.Equal(message, exception.Message); + return exception; + } + + public static T Throws(Assert.ThrowsDelegateWithReturn action, string message) where T : Exception + { + T exception = Assert.Throws(action); + Assert.Equal(message, exception.Message); + return exception; + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs new file mode 100644 index 0000000000..5096687d93 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs @@ -0,0 +1,198 @@ +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 new file mode 100644 index 0000000000..c8e6c20211 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -0,0 +1,2595 @@ +// 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.Globalization; +using System.IO; +using System.Threading; +using Microsoft.AspNet.Abstractions; +using Xunit; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateRouteTests + { + [Fact] + public void GetRouteDataWithConstraintsThatIsNotStringThrows() + { + // Arrange + HttpContext context = GetHttpContext("~/category/33"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = 5 }), + null); + + // Act + Assert.Throws(() => r.GetRouteData(context), + "The constraint entry 'category' on the route with route template 'category/{category}' must have a string value or " + + "be of a type which implements 'ITemplateRouteConstraint'."); + } + + + [Fact] + public void MatchSingleRoute() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction/123"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("Bank", rd.Values["controller"]); + Assert.Equal("DoAction", rd.Values["action"]); + Assert.Equal("123", rd.Values["id"]); + } + + [Fact] + public void NoMatchSingleRoute() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSingleRouteWithDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("Bank", rd.Values["controller"]); + Assert.Equal("DoAction", rd.Values["action"]); + Assert.Equal("default id", rd.Values["id"]); + } + +#if URLGENERATION + + [Fact] + public void MatchSingleRouteWithEmptyDefaults() + { + IHttpVirtualPathData data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val2 = "SomeVal2" })); + Assert.Null(data); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a" })); + Assert.Equal("Test/a", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val3 = "" }), new RouteValueDictionary(new { val2 = "a" })); + Assert.Null(data); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); + Assert.Equal("Test/a/b", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b", val3 = "c" })); + Assert.Equal("Test/a/b/c", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); + Assert.Equal("Test/a/b", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a" })); + Assert.Equal("Test/a", data.VirtualPath); + + } + + private IHttpVirtualPathData GetVirtualPathFromRoute(string path, string template, RouteValueDictionary defaults, RouteValueDictionary values) + { + TemplateRoute r = CreateRoute(template, defaults, null); + + HttpContext context = GetHttpContext(path); + return r.GetVirtualPath(context, values); + } +#endif + + [Fact] + public void NoMatchSingleRouteWithDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/111/bar/222"); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("111", rd.Values["p1"]); + Assert.Equal("222", rd.Values["p2"]); + } + + [Fact] + public void MatchRouteWithLiteralsAndDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/111/bar/"); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("111", rd.Values["p1"]); + Assert.Equal("default p2", rd.Values["p2"]); + } + + [Fact] + public void MatchRouteWithOnlyLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bar", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void NoMatchRouteWithOnlyLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bars", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar/"); + TemplateRoute r = CreateRoute("moo/bar", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void MatchRouteUrlWithExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bar/", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void MatchRouteUrlWithParametersAndExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("{p1}/{p2}/", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal("moo", rd.Values["p1"]); + Assert.Equal("bar", rd.Values["p2"]); + } + + [Fact] + public void NoMatchRouteUrlWithDifferentLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar/boo"); + TemplateRoute r = CreateRoute("{p1}/{p2}/baz", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void NoMatchLongerUrl() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("{p1}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSimpleFilename() + { + // Arrange + HttpContext context = GetHttpContext("~/default.aspx"); + TemplateRoute r = CreateRoute("DEFAULT.ASPX", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + private void VerifyRouteMatchesWithContext(string route, string requestUrl) + { + HttpContext context = GetHttpContext(requestUrl); + TemplateRoute r = CreateRoute(route, null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void MatchEvilRoute() + { + VerifyRouteMatchesWithContext("{prefix}x{suffix}", "~/xxxxxxxxxx"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xxxxyzxyzxxxxxxyz"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/abcxxxxyzxyzxxxxxxyzxx"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyzxyzxyz"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyzxyzxyz1"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyz"); + VerifyRouteMatchesWithContext("{prefix}aa{suffix}", "~/aaaaa"); + VerifyRouteMatchesWithContext("{prefix}aaa{suffix}", "~/aaaaa"); + } + + [Fact] + public void MatchRouteWithExtraDefaultValues() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{p2}", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + Assert.Equal("bar", rd.Values["foo"]); + } + + [Fact] + public void MatchPrettyRouteWithExtraDefaultValues() + { + // Arrange + HttpContext context = GetHttpContext("~/date/2007/08"); + TemplateRoute r = CreateRoute( + "date/{y}/{m}/{d}", + new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(5, rd.Values.Count); + Assert.Equal("blog", rd.Values["controller"]); + Assert.Equal("showpost", rd.Values["action"]); + Assert.Equal("2007", rd.Values["y"]); + Assert.Equal("08", rd.Values["m"]); + Assert.Null(rd.Values["d"]); + } + + [Fact] + public void GetRouteDataWhenConstraintsMatchesExactlyReturnsMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/12"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("12", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly1() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/FooBar"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly2() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/Food"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly3() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/Bar"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("Bar", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataWithCaseInsensitiveConstraintsMatches() + { + // Arrange + HttpContext context = GetHttpContext("~/category/aBc"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"[a-z]{3}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("aBc", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}", null), + "language/en-US", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnLeftEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}a", null), + "language/en-USa", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnRightEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}", null), + "language/aen-US", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/aen-USa", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/a-USa", + null); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/aen-a", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language/en", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language/", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-", null), + "language/en-", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnRightEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}", null), + "language/aen", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}a", null), + "language/aena", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentStandardMvcRouteMatches() + { + GetRouteDataHelper( + CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })), + "home.mvc/index", + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), + "language/-", + null); + } + +#if URLGENERATION + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx-"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}a", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axxa"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + GetVirtualPathHelper( + CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "products.mvc"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "language/zz-yy"); + } + +#endif + + [Fact] + public void GetRouteDataWithUrlWithMultiSegmentWithRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}..mvc/{id}/{Param1}", null), + "Home..mvc/123/p1", + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithTwoRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/../{action}", null), + "Home.mvc/../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithThreeRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/.../{action}", null), + "Home.mvc/.../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithManyRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/../../../{action}", null), + "Home.mvc/../../../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithExclamationPoint() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc!/{action}", null), + "Home.mvc!/index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingDotDotSlash() + { + GetRouteDataHelper( + CreateRoute("../{Controller}.mvc", null), + "../Home.mvc", + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingBackslash() + { + GetRouteDataHelper( + CreateRoute(@"\{Controller}.mvc", null), + @"\Home.mvc", + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithBackslashSeparators() + { + GetRouteDataHelper( + CreateRoute(@"{Controller}.mvc\{id}\{Param1}", null), + @"Home.mvc\123\p1", + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithParenthesesLiterals() + { + GetRouteDataHelper( + CreateRoute(@"(Controller).mvc", null), + @"(Controller).mvc", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSlashSpace() + { + GetRouteDataHelper( + CreateRoute(@"Controller.mvc/ ", null), + @"Controller.mvc/ ", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSpace() + { + GetRouteDataHelper( + CreateRoute(@"Controller.mvc ", null), + @"Controller.mvc ", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + GetRouteDataHelper( + CreateRoute( + "Home/ShowPilot/{missionId}/{*name}", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + null), + "Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } + + [Fact] + public void GetRouteDataWhenConstraintsMatchesPartiallyDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/a12"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWhenConstraintsDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/ab"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWhenOneOfMultipleConstraintsDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/01/q"); + TemplateRoute r = CreateRoute( + "category/{category}/{sort}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d", sort = @"a|d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithNonStringValueReturnsTrueIfMatches() + { + // Arrange + HttpContext context = GetHttpContext("~/category"); + TemplateRoute r = CreateRoute( + "category/{foo}", + new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), + new RouteValueDictionary(new { foo = @"\d{3}" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void GetRouteDataWithNonStringValueReturnsFalseIfUnmatched() + { + // Arrange + HttpContext context = GetHttpContext("~/category"); + TemplateRoute r = CreateRoute( + "category/{foo}", + new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), + new RouteValueDictionary(new { foo = @"\d{2}" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + +#if URLGENERATION + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + GetVirtualPathHelper( + CreateRoute("{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), null), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } + + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "foo/home"); + } + + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + GetVirtualPathHelper( + CreateRoute( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + null), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "oldaction"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "newaction"); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrenIRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); + rt.Add(CreateRoute("{controller}/{action}/{id}", null)); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("{controller}/{action}/{id}")); + rt.Add(CreateRoute("date/{y}/{m}/{d}")); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + valuesDictionary.Add("p3", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(new { p2 = "d2", p3 = "d3", }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + valuesDictionary.Add("p3", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + // DevDiv Bugs 157535 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "orig"); + rd.Values.Add("action", "init"); + rd.Values.Add("id", "123"); + + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }), null); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "new"); + + // Act + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("orig/new", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() + { + // DevDiv Bugs 156606 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("action", "MakeDeposit"); + rd.Values.Add("accountId", "7770"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "{controller}.mvc/Deposit/{accountId}", + new RouteValueDictionary(new { Action = "DepositView" }))); + + // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the + // recent fix to Route (in this changelist) this route would have been selected since we have values for + // all three required parameters. + //rc.Add(new Route { + // Url = "{controller}.mvc/{action}/{accountId}", + // RouteHandler = new DummyRouteHandler() + //}); + + // This route should be chosen because the requested action is List. Since the default value of the action + // is List then the Action should not be in the URL. However, the file extension should be included since + // it is considered "safe." + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "List"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() + { + // DevDiv Bugs 159099 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bar"); + rd.Values.Add("action", "bbb"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); + + rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "aaa"); + values.Add("Controller", "foo"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNoChangedValuesShouldProduceSameUrl() + { + // DevDiv Bugs 159469 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlAppliesConstraintsRulesToChooseRoute() + { + // DevDiv Bugs 159678: MVC: URL generation chooses the wrong route for generating URLs when route validation is in place + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "foo.mvc/{action}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); + + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { action = "Index" }), + new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "UrlRouting"); + rd.Values.Add("action", "Play"); + rd.Values.Add("category", "Photos"); + rd.Values.Add("year", "2008"); + rd.Values.Add("occasion", "Easter"); + rd.Values.Add("SafeParam", "SafeParamValue"); + + TemplateRoute r = CreateRoute( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("year", null); + values.Add("occasion", "Hola"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); + } + + [Fact] + public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() + { + // DevDiv Bugs 172913: UrlRouting: Parameter validation should not run against current request values if a new value has been supplied at a previous position + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "UrlRouting"); + rd.Values.Add("Name", "MissmatchedValidateParams"); + rd.Values.Add("action", "MissmatchedValidateParameters2"); + rd.Values.Add("ValidateParam1", "special1"); + rd.Values.Add("ValidateParam2", "special2"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), + new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); + + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), + new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Name", "MissmatchedValidateParams"); + values.Add("ValidateParam1", "valid1"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "UrlRouting"); + rd.Values.Add("action", "Play"); + rd.Values.Add("category", "Photos"); + rd.Values.Add("year", "2008"); + rd.Values.Add("occasion", "Easter"); + rd.Values.Add("SafeParam", "SafeParamValue"); + + TemplateRoute r = CreateRoute( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + // DevDiv Bugs 172084: UrlRouting: Route.GetUrl generates the wrong route of new values has a different controller and route has an action parameter with default + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "Test"); + rd.Values.Add("Action", "Fallback"); + rd.Values.Add("param1", "fallback1"); + rd.Values.Add("param2", "fallback2"); + rd.Values.Add("param3", "fallback3"); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { Controller = "Test", Action = "Default" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + // The original bug for this included this value, but with the new support for + // creating query string values it changes the behavior such that the URL is + // not what was originally expected. To preserve the general behavior of this + // unit test the 'param2' value is no longer being added. + //values.Add("param2", "a"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("subtest.mvc/Default/b", vpd.VirtualPath); + } + + [Fact] + public void GetUrlVerifyEncoding() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("%23%3b%3f%3a%40%26%3d%2b%24%2c.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "products"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("products.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { controller = "Home", Custom = "customValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "products"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("sort", "desc"); + values.Add("maxPrice", 100); + values.Add("custom", "customValue"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("products.mvc/showcategory/123?sort=desc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("bl%og/{controller}/he llo/{action}", null); + var rd = CreateRouteData(); + rd.Values.Add("controller", "ho%me"); + rd.Values.Add("action", "li st"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("bl%25og/ho%25me/he%20llo/li%20st", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + } + + [Fact] + public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() + { + // DevDiv Bugs 177401: UrlRouting: Incorrect route picked on urlgeneration if using controller from ambient values and route does not have a url parameter for controller + + // DevDiv Bugs 191162: UrlRouting: Route does not match when an ambient route value doesn't match a required default value in the target route + // Because of this bug the test was split into two separate verifications since the original test was verifying slightly incorrect behavior + + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r1 = CreateRoute( + "ParameterMatching.mvc/{Action}/{product}", + new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), + null); + + TemplateRoute r2 = CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }), + new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), + null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("Action", "List"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "AttemptLogin"); + + // Act for first route + var vpd = r1.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); + + // Act for second route + vpd = r2.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); + } + +#endif + [Fact] + public void RouteWithCatchAllClauseCapturesManySlashes() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/v2/v3"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("v2/v3", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesTrailingSlash() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("catchall", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/hello/whatever"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("hello/whatever", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllRejectsConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/abcd"); + TemplateRoute r = CreateRoute( + "{p1}/{*p2}", + new RouteValueDictionary(new { p2 = "catchall" }), + new RouteValueDictionary(new { p2 = "\\d{4}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void RouteWithCatchAllAcceptsConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/1234"); + TemplateRoute r = CreateRoute( + "{p1}/{*p2}", + new RouteValueDictionary(new { p2 = "catchall" }), + new RouteValueDictionary(new { p2 = "\\d{4}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("1234", rd.Values["p2"]); + } + +#if URLGENERATION + + [Fact] + public void GetUrlWithCatchAllWithValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", "v2a/v2b"); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1/v2a/v2b", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", null); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(2, vpd.DataTokens.Count); + Assert.Equal("bar", vpd.DataTokens["foo"]); + Assert.Equal("quux", vpd.DataTokens["qux"]); + } + + [Fact] + public void GetVirtualPathWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetVirtualPathWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "list"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + +#if DATATOKENS + + [Fact] + public void GetUrlWithCatchAllWithAmbientValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null, null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + rd.Values.Add("p2", "ambient-catch-all"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1/ambient-catch-all", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(0, vpd.DataTokens.Count); + } +#endif +#endif + +#if DATATOKENS + + [Fact] + public void GetRouteDataWithDataTokensCopiesThemFromRouteToIRouteData() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category/33", null); + TemplateRoute r = CreateRoute("category/{category}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(1, rd.Values.Count); + Assert.Equal(2, rd.DataTokens.Count); + Assert.Equal("33", rd.Values["category"]); + Assert.Equal("bar", rd.DataTokens["foo"]); + Assert.Equal("quux", rd.DataTokens["qux"]); + } + +#endif + + [Fact] + public void GetRouteDataWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/home/index"); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("home", rd.Values["controller"]); + Assert.Equal("index", rd.Values["action"]); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetRouteDataWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/home/list"); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetRouteDataWithConstraintIsCultureInsensitive() + { + // Arrange + HttpContext context = GetHttpContext("~/category/\u0130"); // Turkish upper-case dotted I + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"[a-z]+" }), + null); + + // Act + Thread currentThread = Thread.CurrentThread; + CultureInfo backupCulture = currentThread.CurrentCulture; + RouteMatch rd; + try + { + currentThread.CurrentCulture = new CultureInfo("tr-TR"); // Turkish culture + rd = r.GetRouteData(context); + } + finally + { + currentThread.CurrentCulture = backupCulture; + } + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithConstraintThatHasNoValueDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category/33"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { foo = @"\d\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithCatchAllConstraintThatHasNoValueDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category"); + TemplateRoute r = CreateRoute( + "category/{*therest}", + null, + new RouteValueDictionary(new { therest = @"hola" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void ProcessConstraintShouldGetCalledForCustomConstraintDuringUrlGeneration() + { + // DevDiv Bugs 178588: UrlRouting: ProcessConstraint is not invoked on a custom constraint that is not mapped to a url parameter during urlgeneration + + // Arrange + HttpContext context = GetHttpContext("/app", null); + + DevDivBugs178588CustomRoute r = new DevDivBugs178588CustomRoute( + "CustomPath.mvc/{action}/{param1}/{param2}", + new RouteValueDictionary(new { Controller = "Test" }), + new RouteValueDictionary(new { foo = new DevDivBugs178588CustomConstraint() })); + + var rd = CreateRouteData(); + rd.Values.Add("action", "Test"); + rd.Values.Add("param1", "111"); + rd.Values.Add("param2", "222"); + rd.Values.Add("Controller", "Test"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario1a() + { + TemplateRoute r = CreateRoute( + "CatchAllParamsWithDefaults/{Controller}.mvc/{Action}/{*therest}", + new RouteValueDictionary(new { therest = "Hello" }), + new RouteValueDictionary(new { Controller = "CatchAllParams" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 1.a. + GetRouteDataHelper( + r, + "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", + new RouteValueDictionary(new { Controller = "CatchAllParams", Action = "TestCatchAllParamInIRouteData", therest = "Hello" })); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario1b() + { + TemplateRoute r = CreateRoute( + "CatchAllParams/{Controller}.mvc/{Action}/{*therest}", + null, + new RouteValueDictionary(new { Controller = "CatchAllParams" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 1.b. + GetRouteDataHelper( + r, + "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", + null); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario2() + { + TemplateRoute r = CreateRoute( + "{controller}.mvc/Login", + new RouteValueDictionary(new { Action = "LoginView" }), + new RouteValueDictionary(new { Controller = "Bank" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 2 + GetRouteDataHelper( + r, + "Bank.mvc/AttemptLogin", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyLeftLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "fooBAR", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyRightLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "BARfoo", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchMiddleLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "BARfooBAR", + null); + } + + [Fact] + public void GetRouteDataDoesMatchesExactLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "foo", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithWeirdParameterNames() + { + TemplateRoute r = CreateRoute( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + null); + + GetRouteDataHelper( + r, + "foo/space/weird/orderid", + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } + +#if URLGENERATION + + [Fact] + public void UrlWithEscapedOpenCloseBraces() + { + RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); + } + + private static void RouteFormatHelper(string routeUrl, string requestUrl) + { + RouteValueDictionary defaults = new RouteValueDictionary(new { route = "matched" }); + TemplateRoute r = CreateRoute(routeUrl, defaults, null); + + GetRouteDataHelper(r, requestUrl, defaults); + GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheEnd() + { + RouteFormatHelper("bar{{", "bar{"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheBeginning() + { + RouteFormatHelper("{{bar", "{bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedOpenBrace() + { + RouteFormatHelper("foo{{{{bar", "foo{{bar"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheEnd() + { + RouteFormatHelper("bar}}", "bar}"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheBeginning() + { + RouteFormatHelper("}}bar", "}bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedCloseBrace() + { + RouteFormatHelper("foo}}}}bar", "foo}}bar"); + } + + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + // DevDiv Bugs 194371: UrlRouting: Exception thrown when generating URL that has some null values + GetVirtualPathHelper( + CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + null), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "Home.mvc/TestAction/1"); + } + + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + GetVirtualPathHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "Orders/en-US"); + } +#endif + + [Fact] + public void GetRouteDataDoesNotMatchRouteWithLiteralSeparatorDefaultsButNoValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndLeftValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/xx-", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndRightValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/-yy", + null); + } + + [Fact] + public void GetRouteDataMatchesRouteWithLiteralSeparatorDefaultsAndValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/xx-yy", + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } + +#if URLGENERATION + + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter + GetVirtualPathHelper( + CreateRoute("{Controller}.mvc/{action}/{end}", null, new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), null), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), + "Orders.mvc/Index/end"); + } + + [Fact] + public void DefaultRoutingValuesTestWithStringEmpty() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = "", val3 = "" }), new RouteValueDictionary()); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11", val3 = "", val4 = "" })); + Assert.Equal("Test/42/11", data.VirtualPath); + + } + + [Fact] + public void MixedDefaultAndExplicitRoutingValuesTestWithStringEmpty() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "42" })); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); + Assert.Equal("Test/42/11", data.VirtualPath); + } + + [Fact] + public void DefaultRoutingValuesTestWithNull() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary()); + Assert.Equal("Test/42", data.VirtualPath); + } + + [Fact] + public void MixedDefaultAndExplicitRoutingValuesTestWithNull() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary(new { val1 = "42" })); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); + Assert.Equal("Test/42/11", data.VirtualPath); + } + +#endif + + private static IRouteValues CreateRouteData() + { + return new RouteValues(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + private static RouteValueDictionary CreateRouteValueDictionary() + { + var values = new RouteValueDictionary(); + return values; + } + + private static void GetRouteDataHelper(TemplateRoute route, string requestPath, RouteValueDictionary expectedValues) + { + // Arrange + HttpContext context = GetHttpContext(requestPath); + + // Act + var rd = route.GetRouteData(context); + + // Assert + if (expectedValues == null) + { + Assert.Null(rd); + } + else + { + Assert.NotNull(rd); + Assert.Equal(rd.Values.Count, expectedValues.Count); + foreach (string key in rd.Values.Keys) + { + Assert.Equal(expectedValues[key], rd.Values[key]); + } + } + } + +#if URLGENERATION + private static void GetVirtualPathHelper(TemplateRoute route, RouteValueDictionary currentValues, RouteValueDictionary newValues, string expectedPath) + { + // Arrange + newValues = newValues ?? new RouteValueDictionary(); + + HttpContext context = GetHttpContext("/app", String.Empty, null); + var rd = CreateRouteData(); + foreach (var currentValue in currentValues) + { + rd.Values.Add(currentValue.Key, currentValue.Value); + } + + // Act + var vpd = route.GetVirtualPath(context, newValues); + + // Assert + if (expectedPath == null) + { + Assert.Null(vpd); + } + else + { + Assert.NotNull(vpd); + Assert.Equal(expectedPath, vpd.VirtualPath); + } + } + +#endif + private static ITemplateRouteConstraint CreateHttpMethodConstraint(params string[] methods) + { + return null; + } + + internal static HttpContext GetHttpContext(string requestPath) + { + return GetHttpContext(null, requestPath); + } + + private static HttpContext GetHttpContext(string appPath, string requestPath) + { + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '~') + { + requestPath = requestPath.Substring(1); + } + + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] != '/') + { + requestPath = "/" + requestPath; + } + + var context = new MockHttpContext(); + context.Request.Path = new PathString(requestPath); + context.Request.PathBase = new PathString(appPath); + + return context; + } + + private static TemplateRoute CreateRoute(string template) + { + return CreateRoute(template, null, null, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults) + { + return CreateRoute(template, defaults, null, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints) + { + return CreateRoute(template, defaults, constraints, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) + { + return new TemplateRoute(template, defaults, constraints, dataTokens); + } + + private class DevDivBugs178588CustomConstraint + { + public string AllowedHeader + { + get; + set; + } + } + + private class DevDivBugs178588CustomRoute : TemplateRoute + { + public DevDivBugs178588CustomRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) + : base(url, defaults, constraints, null) + { + } + + protected override bool ProcessConstraint(HttpContext httpContext, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + if (constraint is DevDivBugs178588CustomConstraint) + { + return false; + } + else + { + return base.ProcessConstraint(httpContext, constraint, parameterName, values, routeDirection); + } + } + } + + private sealed class ConstraintData + { + public object Constraint + { + get; + set; + } + public string ParameterName + { + get; + set; + } + public object ParameterValue + { + get; + set; + } + } + + private class CustomConstraintTemplateRoute : TemplateRoute + { + public CustomConstraintTemplateRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) + : base(url, defaults, constraints, null) + { + } + + public ConstraintData ConstraintData + { + get; + set; + } + + protected override bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + object parameterValue; + values.TryGetValue(parameterName, out parameterValue); + + // Save the parameter values to validate them in the unit tests + ConstraintData = new ConstraintData + { + Constraint = constraint, + ParameterName = parameterName, + ParameterValue = parameterValue, + }; + + if (constraint is int) + { + int lengthRequirement = (int)constraint; + string paramString = parameterValue as string; + if (paramString == null) + { + throw new InvalidOperationException("This constraint only works with string values."); + } + return (paramString.Length == lengthRequirement); + } + else + { + return base.ProcessConstraint(request, constraint, parameterName, values, routeDirection); + } + } + } + + // This is a placeholder + private class RouteValueDictionary : Dictionary + { + public RouteValueDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public RouteValueDictionary(object obj) + : base(StringComparer.OrdinalIgnoreCase) + { + foreach (var property in obj.GetType().GetProperties()) + { + Add(property.Name, property.GetValue(obj)); + } + } + } + + private class MockHttpContext : HttpContext + { + private readonly Dictionary _features = new Dictionary(); + private readonly MockHttpRequest _request; + + public MockHttpContext() + { + _request = new MockHttpRequest(this); + } + + public override void Dispose() + { + } + + public override object GetFeature(Type type) + { + return _features[type]; + } + + public override IDictionary Items + { + get { throw new NotImplementedException(); } + } + + public override HttpRequest Request + { + get { return _request; } + } + + public override HttpResponse Response + { + get { throw new NotImplementedException(); } + } + + public override void SetFeature(Type type, object instance) + { + _features[type] = instance; + } + } + + private class MockHttpRequest : HttpRequest + { + private readonly HttpContext _context; + public MockHttpRequest(HttpContext context) + { + _context = context; + } + + public override Stream Body + { + get; + set; + } + + public override CancellationToken CallCanceled + { + get; + set; + } + + public override IReadableStringCollection Cookies + { + get { throw new NotImplementedException(); } + } + + public override IHeaderDictionary Headers + { + get { throw new NotImplementedException(); } + } + + public override HostString Host + { + get; + set; + } + + public override HttpContext HttpContext + { + get { return _context; } + } + + public override bool IsSecure + { + get { throw new NotImplementedException(); } + } + + public override string Method + { + get; + set; + } + + public override PathString Path + { + get; + set; + } + + public override PathString PathBase + { + get; + set; + } + + public override string Protocol + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override IReadableStringCollection Query + { + get { throw new NotImplementedException(); } + } + + public override QueryString QueryString + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override string Scheme + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/project.json b/test/Microsoft.AspNet.Routing.Tests/project.json index 5168efba28..6261e25ace 100644 --- a/test/Microsoft.AspNet.Routing.Tests/project.json +++ b/test/Microsoft.AspNet.Routing.Tests/project.json @@ -1,12 +1,15 @@ { "version": "0.1-alpha-*", "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", "Microsoft.AspNet.Routing" : "" }, "configurations": { "net45": { "dependencies": { - "Owin": "1.0" + "Owin": "1.0", + "xunit": "1.9.2", + "xunit.extensions": "1.9.2" } } }