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