Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/mono.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel de Icaza <miguel@gnome.org>2013-05-22 06:36:02 +0400
committerMiguel de Icaza <miguel@gnome.org>2013-05-22 06:36:02 +0400
commitaaf6c2113924ec03ed09c1580dd88728c14b4570 (patch)
tree4dd45a05766d263cfffea2e6dd96678f9d2a850f
parent4958a276a6c7b5ef671e022e1632703d7752d253 (diff)
parent106c059d9f15ef02bd374bc613c611081abeeeb1 (diff)
Merge pull request #629 from pruiz/syswebrouting-fixes2
[Sys.Web.Routing] Refactor of Route's GetVirtualPath implementation based on System.Web.Http.Routing code in order to fix a few incompatibilities between our implementation and the one at ms.net
-rw-r--r--mcs/class/System.Web.Routing/System.Web.Routing/PatternParser.cs456
-rw-r--r--mcs/class/System.Web.Routing/Test/System.Web.Routing/RouteTest.cs208
2 files changed, 520 insertions, 144 deletions
diff --git a/mcs/class/System.Web.Routing/System.Web.Routing/PatternParser.cs b/mcs/class/System.Web.Routing/System.Web.Routing/PatternParser.cs
index 5c8b07a1358..ea321dc4692 100644
--- a/mcs/class/System.Web.Routing/System.Web.Routing/PatternParser.cs
+++ b/mcs/class/System.Web.Routing/System.Web.Routing/PatternParser.cs
@@ -32,8 +32,11 @@ using System;
using System.Collections.Generic;
using System.Security.Permissions;
using System.Text;
+using System.Text.RegularExpressions;
using System.Web;
using System.Web.Util;
+using System.Diagnostics;
+using System.Globalization;
namespace System.Web.Routing
{
@@ -207,6 +210,61 @@ namespace System.Web.Routing
return dict;
}
+ static bool ParametersAreEqual (object a, object b)
+ {
+ if (a is string && b is string) {
+ return String.Equals (a as string, b as string, StringComparison.OrdinalIgnoreCase);
+ } else {
+ // Parameter may be a boxed value type, need to use .Equals() for comparison
+ return object.Equals (a, b);
+ }
+ }
+
+ static bool ParameterIsNonEmpty (object param)
+ {
+ if (param is string)
+ return !string.IsNullOrEmpty (param as string);
+
+ return param != null;
+ }
+
+ bool IsParameterRequired (string parameterName, RouteValueDictionary defaultValues, out object defaultValue)
+ {
+ foreach (var token in tokens) {
+ if (token == null)
+ continue;
+
+ if (string.Equals (token.Name, parameterName, StringComparison.OrdinalIgnoreCase)) {
+ if (token.Type == PatternTokenType.CatchAll) {
+ defaultValue = null;
+ return false;
+ }
+ }
+ }
+
+ if (defaultValues == null)
+ throw new ArgumentNullException ("defaultValues is null?!");
+
+ return !defaultValues.TryGetValue (parameterName, out defaultValue);
+ }
+
+ static string EscapeReservedCharacters (Match m)
+ {
+ if (m == null)
+ throw new ArgumentNullException("m");
+
+ return Uri.HexEscape (m.Value[0]);
+ }
+
+ static string UriEncode (string str)
+ {
+ if (string.IsNullOrEmpty (str))
+ return str;
+
+ string escape = Uri.EscapeUriString (str);
+ return Regex.Replace (escape, "([#?])", new MatchEvaluator (EscapeReservedCharacters));
+ }
+
bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
{
string pathSegment = argSegs [segIndex];
@@ -373,180 +431,290 @@ namespace System.Web.Routing
return null;
RouteData routeData = requestContext.RouteData;
- RouteValueDictionary defaultValues = route != null ? route.Defaults : null;
- RouteValueDictionary ambientValues = routeData.Values;
-
- if (defaultValues != null && defaultValues.Count == 0)
- defaultValues = null;
- if (ambientValues != null && ambientValues.Count == 0)
- ambientValues = null;
- if (userValues != null && userValues.Count == 0)
- userValues = null;
-
- // Check URL parameters
- // It is allowed to take ambient values for required parameters if:
- //
- // - there are no default values provided
- // - the default values dictionary contains at least one required
- // parameter value
- //
- bool canTakeFromAmbient;
- if (defaultValues == null)
- canTakeFromAmbient = true;
- else {
- canTakeFromAmbient = false;
- foreach (KeyValuePair <string, bool> de in parameterNames) {
- if (defaultValues.ContainsKey (de.Key)) {
- canTakeFromAmbient = true;
+ var currentValues = routeData.Values ?? new RouteValueDictionary ();
+ var values = userValues ?? new RouteValueDictionary ();
+ var defaultValues = (route != null ? route.Defaults : null) ?? new RouteValueDictionary ();
+
+ // The set of values we should be using when generating the URL in this route
+ var acceptedValues = new RouteValueDictionary ();
+
+ // Keep track of which new values have been used
+ HashSet<string> unusedNewValues = new HashSet<string> (values.Keys, StringComparer.OrdinalIgnoreCase);
+
+ // This route building logic is based on System.Web.Http's Routing code (which is Apache Licensed by MS)
+ // and which can be found at mono's external/aspnetwebstack/src/System.Web.Http/Routing/HttpParsedRoute.cs
+ // Hopefully this will ensure a much higher compatiblity with MS.NET's System.Web.Routing logic. (pruiz)
+
+ #region Step 1: Get the list of values we're going to use to match and generate this URL
+ // Find out which entries in the URL are valid for the URL we want to generate.
+ // If the URL 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=<no value>.
+ foreach (var item in parameterNames) {
+ var parameterName = item.Key;
+
+ 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 (!ParametersAreEqual (currentParameterValue, newParameterValue)) {
+ // Stop copying current values when we find one that doesn't match
break;
}
}
- }
-
- bool allMustBeInUserValues = false;
- foreach (KeyValuePair <string, bool> de in parameterNames) {
- string parameterName = de.Key;
- // Is the parameter required?
- if (defaultValues == null || !defaultValues.ContainsKey (parameterName)) {
- // Yes, it is required (no value in defaults)
- // Has the user provided value for it?
- if (userValues == null || !userValues.ContainsKey (parameterName)) {
- if (allMustBeInUserValues)
- return null; // partial override => no match
-
- if (!canTakeFromAmbient || ambientValues == null || !ambientValues.ContainsKey (parameterName))
- return null; // no value provided => no match
- } else if (canTakeFromAmbient)
- allMustBeInUserValues = true;
+
+ // If the parameter is a match, add it to the list of values we will use for URL generation
+ if (hasNewParameterValue) {
+ if (ParameterIsNonEmpty (newParameterValue)) {
+ acceptedValues.Add (parameterName, newParameterValue);
+ }
+ }
+ else {
+ if (hasCurrentParameterValue) {
+ acceptedValues.Add (parameterName, currentParameterValue);
+ }
}
}
- // Check for non-url parameters
- if (defaultValues != null) {
- foreach (var de in defaultValues) {
- string parameterName = de.Key;
-
- if (parameterNames.ContainsKey (parameterName))
- continue;
-
- object parameterValue = null;
- // Has the user specified value for this parameter and, if
- // yes, is it the same as the one in defaults?
- if (userValues != null && userValues.TryGetValue (parameterName, out parameterValue)) {
- object defaultValue = de.Value;
- if (defaultValue is string && parameterValue is string) {
- if (String.Compare ((string)defaultValue, (string)parameterValue, StringComparison.OrdinalIgnoreCase) != 0)
- return null; // different value => no match
- // Parameter may be a boxed value type, need to use .Equals() for comparison
- } else if (!object.Equals (parameterValue, defaultValue))
- return null; // different value => no match
- }
+ // Add all remaining new values to the list of values we will use for URL generation
+ foreach (var newValue in values) {
+ if (ParameterIsNonEmpty (newValue.Value) && !acceptedValues.ContainsKey (newValue.Key)) {
+ acceptedValues.Add (newValue.Key, newValue.Value);
}
}
- // We're a match, generate the URL
- var ret = new StringBuilder ();
- usedValues = new RouteValueDictionary ();
- bool canTrim = true;
-
- // Going in reverse order, so that we can trim without much ado
- int tokensCount = tokens.Length - 1;
- for (int i = tokensCount; i >= 0; i--) {
- PatternToken token = tokens [i];
- if (token == null) {
- if (i < tokensCount && ret.Length > 0 && ret [0] != '/')
- ret.Insert (0, '/');
- continue;
+ // Add all current values that aren't in the URL at all
+ foreach (var currentValue in currentValues) {
+ if (!acceptedValues.ContainsKey (currentValue.Key) && !parameterNames.ContainsKey (currentValue.Key)) {
+ acceptedValues.Add (currentValue.Key, currentValue.Value);
}
-
- if (token.Type == PatternTokenType.Literal) {
- ret.Insert (0, token.Name);
- continue;
+ }
+
+ // Add all remaining default values from the route to the list of values we will use for URL generation
+ foreach (var item in parameterNames) {
+ object defaultValue;
+ if (!acceptedValues.ContainsKey (item.Key) && !IsParameterRequired (item.Key, 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 (item.Key, defaultValue);
}
+ }
- string parameterName = token.Name;
- object tokenValue;
+ // All required parameters in this URL must have values from somewhere (i.e. the accepted values)
+ foreach (var item in parameterNames) {
+ object defaultValue;
+ if (IsParameterRequired (item.Key, defaultValues, out defaultValue) && !acceptedValues.ContainsKey (item.Key)) {
+ // 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 null;
+ }
+ }
-#if SYSTEMCORE_DEP
- if (userValues.GetValue (parameterName, out tokenValue)) {
- if (tokenValue != null)
- usedValues.Add (parameterName, tokenValue.ToString ());
+ // All other default values must match if they are explicitly defined in the new values
+ var otherDefaultValues = new RouteValueDictionary (defaultValues);
+ foreach (var item in parameterNames) {
+ otherDefaultValues.Remove (item.Key);
+ }
- if (!defaultValues.Has (parameterName, tokenValue)) {
- canTrim = false;
- if (tokenValue != null)
- ret.Insert (0, tokenValue.ToString ());
- continue;
+ foreach (var defaultValue in otherDefaultValues) {
+ object value;
+ if (values.TryGetValue (defaultValue.Key, out value)) {
+ unusedNewValues.Remove (defaultValue.Key);
+ if (!ParametersAreEqual (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;
}
-
- if (!canTrim && tokenValue != null)
- ret.Insert (0, tokenValue.ToString ());
-
- continue;
}
+ }
+ #endregion
- if (defaultValues.GetValue (parameterName, out tokenValue)) {
- object ambientTokenValue;
- if (ambientValues.GetValue (parameterName, out ambientTokenValue))
- tokenValue = ambientTokenValue;
+ #region Step 2: If the route is a match generate the appropriate URL
- if (!canTrim && tokenValue != null)
- ret.Insert (0, tokenValue.ToString ());
+ var uri = new StringBuilder ();
+ var pendingParts = new StringBuilder ();
+ var pendingPartsAreAllSafe = false;
+ bool blockAllUriAppends = false;
+ var allSegments = new List<PatternSegment?> ();
- usedValues.Add (parameterName, tokenValue.ToString ());
- continue;
- }
+ // Build a list of segments plus separators we can use as template.
+ foreach (var segment in segments) {
+ if (allSegments.Count > 0)
+ allSegments.Add (null); // separator exposed as null.
+ allSegments.Add (segment);
+ }
- canTrim = false;
- if (ambientValues.GetValue (parameterName, out tokenValue)) {
- if (tokenValue != null)
- {
- ret.Insert (0, tokenValue.ToString ());
- usedValues.Add (parameterName, tokenValue.ToString ());
+ // Finally loop thru al segment-templates building the actual uri.
+ foreach (var item in allSegments) {
+ var segment = item.GetValueOrDefault ();
+
+ // If segment is a separator..
+ if (item == null) {
+ if (pendingPartsAreAllSafe) {
+ // Accept
+ if (pendingParts.Length > 0) {
+ if (blockAllUriAppends)
+ return null;
+
+ // Append any pending literals to the URL
+ uri.Append (pendingParts.ToString ());
+ pendingParts.Length = 0;
+ }
}
- continue;
- }
+ 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;
+ } else {
+ pendingParts.Append ("/");
+ }
+#if false
+ } else if (segment.AllLiteral) {
+ // Spezial (optimized) case: all elements of segment are literals.
+ pendingPartsAreAllSafe = true;
+ foreach (var tk in segment.Tokens)
+ pendingParts.Append (tk.Name);
#endif
- }
+ } else {
+ // 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 URL, 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 (var token in segment.Tokens) {
+ if (token.Type == PatternTokenType.Literal) {
+ // If it's a literal we hold on to it until we are sure we need to add it
+ pendingPartsAreAllSafe = true;
+ pendingParts.Append (token.Name);
+ } else {
+ if (token.Type == PatternTokenType.Standard) {
+ if (pendingPartsAreAllSafe) {
+ // Accept
+ if (pendingParts.Length > 0) {
+ if (blockAllUriAppends)
+ return null;
+
+ // Append any pending literals to the URL
+ 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 (token.Name, out acceptedParameterValue);
+ if (hasAcceptedParameterValue)
+ unusedNewValues.Remove (token.Name);
+
+ object defaultParameterValue;
+ defaultValues.TryGetValue (token.Name, out defaultParameterValue);
+
+ if (ParametersAreEqual (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 URL we generate.
+ pendingParts.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
+ } else {
+ if (blockAllUriAppends)
+ return null;
+
+ // Add the new part to the URL as well as any pending parts
+ if (pendingParts.Length > 0) {
+ // Append any pending literals to the URL
+ uri.Append (pendingParts.ToString ());
+ pendingParts.Length = 0;
+ }
+ uri.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
+
+ addedAnySubsegments = true;
+ }
+ } else {
+ Debug.Fail ("Invalid path subsegment type");
+ }
+ }
+ }
- // All the values specified in userValues that aren't part of the original
- // URL, the constraints or defaults collections are treated as overflow
- // values - they are appended as query parameters to the URL
- if (userValues != null) {
- bool first = true;
- foreach (var de in userValues) {
- string parameterName = de.Key;
+ if (addedAnySubsegments) {
+ // See comment above about why we add the pending parts
+ if (pendingParts.Length > 0) {
+ if (blockAllUriAppends)
+ return null;
-#if SYSTEMCORE_DEP
- if (parameterNames.ContainsKey (parameterName) || defaultValues.Has (parameterName) || constraints.Has (parameterName))
- continue;
-#endif
+ // Append any pending literals to the URL
+ uri.Append (pendingParts.ToString ());
+ pendingParts.Length = 0;
+ }
+ }
+ }
+ }
- object parameterValue = de.Value;
- if (parameterValue == null)
- continue;
+ if (pendingPartsAreAllSafe) {
+ // Accept
+ if (pendingParts.Length > 0) {
+ if (blockAllUriAppends)
+ return null;
- var parameterValueAsString = parameterValue as string;
- if (parameterValueAsString != null && parameterValueAsString.Length == 0)
- continue;
-
- if (first) {
- ret.Append ('?');
- first = false;
- } else
- ret.Append ('&');
+ // Append any pending literals to the URI
+ uri.Append (pendingParts.ToString ());
+ }
+ }
-
- ret.Append (Uri.EscapeDataString (parameterName));
- ret.Append ('=');
- if (parameterValue != null)
- ret.Append (Uri.EscapeDataString (de.Value.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);
+ }
+ }
- usedValues.Add (parameterName, de.Value.ToString ());
+ // Encode the URI before we append the query string, otherwise we would double encode the query string
+ var 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 ret.ToString ();
+
+ #endregion
+
+ usedValues = acceptedValues;
+ return uri.ToString();
}
}
}
diff --git a/mcs/class/System.Web.Routing/Test/System.Web.Routing/RouteTest.cs b/mcs/class/System.Web.Routing/Test/System.Web.Routing/RouteTest.cs
index 27328c67bec..4ac8cbcdcd9 100644
--- a/mcs/class/System.Web.Routing/Test/System.Web.Routing/RouteTest.cs
+++ b/mcs/class/System.Web.Routing/Test/System.Web.Routing/RouteTest.cs
@@ -1117,6 +1117,53 @@ namespace MonoTests.System.Web.Routing
}
[Test]
+ public void GetVirtualPath4_2 ()
+ {
+ var r = new MyRoute("{foo}/{bar}", new MyRouteHandler());
+ var hc = new HttpContextStub2("~/x/y", String.Empty);
+ var rd = r.GetRouteData(hc);
+
+ // override a value incompletely
+ var values = new RouteValueDictionary();
+ values["bar"] = "A";
+
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+ Assert.IsNotNull(vp);
+ Assert.AreEqual("x/A", vp.VirtualPath);
+ }
+
+ [Test]
+ public void GetVirtualPath4Bis ()
+ {
+ var r = new MyRoute("part/{foo}/{bar}", new MyRouteHandler());
+ var hc = new HttpContextStub2("~/part/x/y", String.Empty);
+ var rd = r.GetRouteData(hc);
+
+ // override a value incompletely
+ var values = new RouteValueDictionary();
+ values["foo"] = "A";
+
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+ Assert.IsNull(vp);
+ }
+
+ [Test]
+ public void GetVirtualPath4_2Bis ()
+ {
+ var r = new MyRoute("part/{foo}/{bar}", new MyRouteHandler());
+ var hc = new HttpContextStub2("~/part/x/y", String.Empty);
+ var rd = r.GetRouteData(hc);
+
+ // override a value incompletely
+ var values = new RouteValueDictionary();
+ values["bar"] = "A";
+
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+ Assert.IsNotNull(vp);
+ Assert.AreEqual("part/x/A", vp.VirtualPath);
+ }
+
+ [Test]
public void GetVirtualPath5 ()
{
var r = new MyRoute ("{foo}/{bar}", new MyRouteHandler ());
@@ -1491,6 +1538,167 @@ namespace MonoTests.System.Web.Routing
Assert.AreEqual ("HelloWorld", uppercase.VirtualPath, "#A6");
}
+ [Test]
+ public void GetVirtualPath20 ()
+ {
+ var r = new MyRoute("summary/{controller}/{id}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary(new { action = "Index" })
+ };
+ var hc = new HttpContextStub2("~/summary/kind/1/test", String.Empty);
+ var rd = r.GetRouteData(hc);
+ Assert.IsNotNull(rd, "#1");
+
+ var values = new RouteValueDictionary(new { id = "2", action = "save" });
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#2");
+ Assert.AreEqual("summary/kind/2/save", vp.VirtualPath, "#2-1");
+ Assert.AreEqual(r, vp.Route, "#2-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#2-3");
+
+ values = new RouteValueDictionary(new { id = "3", action = "save", extra = "stuff" });
+ vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#3");
+ Assert.AreEqual("summary/kind/3/save?extra=stuff", vp.VirtualPath, "#3-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#3-3");
+ }
+
+ [Test]
+ public void GetVirtualPath21 ()
+ {
+ var r = new MyRoute("summary/{controller}/{id}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary(new { action = "Index" })
+ };
+ var hc = new HttpContextStub2("~/summary/kind/1/test", String.Empty);
+ var rd = r.GetRouteData(hc);
+ Assert.IsNotNull(rd, "#1");
+ Assert.AreEqual("1", rd.Values["id"]);
+
+ var values = new RouteValueDictionary(new { action = "save" });
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#2");
+ Assert.AreEqual("summary/kind/1/save", vp.VirtualPath, "#2-1");
+ Assert.AreEqual(r, vp.Route, "#2-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#2-3");
+
+ values = new RouteValueDictionary(new { action = "save", extra = "stuff" });
+ vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#3");
+ Assert.AreEqual("summary/kind/1/save?extra=stuff", vp.VirtualPath, "#3-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#3-3");
+ }
+
+ [Test]
+ public void GetVirtualPath22 ()
+ {
+ var r = new MyRoute("summary/{controller}/{id}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary(new { action = "Index" })
+ };
+ var hc = new HttpContextStub2("~/summary/kind/90941a4f-daf3-4c89-a6dc-83e8de4e3db5/test", String.Empty);
+ var rd = r.GetRouteData(hc);
+ Assert.IsNotNull(rd, "#0");
+ Assert.AreEqual("90941a4f-daf3-4c89-a6dc-83e8de4e3db5", rd.Values["id"]);
+
+ var values = new RouteValueDictionary(new { action = "Index" });
+ var vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#1");
+ Assert.AreEqual("summary/kind/90941a4f-daf3-4c89-a6dc-83e8de4e3db5", vp.VirtualPath, "#1-1");
+ Assert.AreEqual(r, vp.Route, "#1-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#1-3");
+
+ values = new RouteValueDictionary(new { action = "save" });
+ vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#2");
+ Assert.AreEqual("summary/kind/90941a4f-daf3-4c89-a6dc-83e8de4e3db5/save", vp.VirtualPath, "#2-1");
+ Assert.AreEqual(r, vp.Route, "#2-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#2-3");
+
+ values = new RouteValueDictionary(new { action = "save", extra = "stuff" });
+ vp = r.GetVirtualPath(new RequestContext(hc, rd), values);
+
+ Assert.IsNotNull(vp, "#3");
+ Assert.AreEqual("summary/kind/90941a4f-daf3-4c89-a6dc-83e8de4e3db5/save?extra=stuff", vp.VirtualPath, "#3-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#3-3");
+ }
+
+ [Test]
+ public void GetVirtualPath23 ()
+ {
+ var r0 = new MyRoute ("summary/{id}", new MyRouteHandler());
+ var r1 = new MyRoute ("summary/{controller}/{id}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary (new { action = "Index" })
+ };
+ var hc = new HttpContextStub2 ("~/summary/90941a4f-daf3-4c89-a6dc-83e8de4e3db5", String.Empty);
+ var rd = r0.GetRouteData (hc);
+ Assert.IsNotNull (rd, "#0");
+ Assert.AreEqual ("90941a4f-daf3-4c89-a6dc-83e8de4e3db5", rd.Values["id"]);
+
+ var values = new RouteValueDictionary ()
+ {
+ { "controller", "SomeThing" },
+ { "action", "Index" }
+ };
+ var vp = r1.GetVirtualPath (new RequestContext (hc, rd), values);
+
+ Assert.IsNotNull (vp, "#1");
+ Assert.AreEqual ("summary/SomeThing/90941a4f-daf3-4c89-a6dc-83e8de4e3db5", vp.VirtualPath, "#1-1");
+ Assert.AreEqual (r1, vp.Route, "#1-2");
+ Assert.AreEqual (0, vp.DataTokens.Count, "#1-3");
+ }
+
+ [Test]
+ public void GetVirtualPath24 ()
+ {
+ var r = new MyRoute ("{controller}/{country}-{locale}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary (new { action = "Index", country = "us", locale = "en" })
+ };
+ var hc = new HttpContextStub2 ("~/login", String.Empty);
+ var rd = r.GetRouteData (hc);
+ Assert.IsNull (rd, "#0");
+
+ var values = new RouteValueDictionary ()
+ {
+ { "controller", "SomeThing" },
+ { "action", "Index" },
+ { "country", "es" }
+ };
+ var vp = r.GetVirtualPath (new RequestContext (hc, new RouteData()), values);
+
+ Assert.IsNotNull (vp, "#1");
+ Assert.AreEqual ("SomeThing/es-en", vp.VirtualPath, "#1-1");
+ Assert.AreEqual (r, vp.Route, "#1-2");
+ Assert.AreEqual (0, vp.DataTokens.Count, "#1-3");
+
+ // Case #2: pass no country, but locale as user value.
+ values.Remove("country");
+ values.Add("locale", "xx");
+ vp = r.GetVirtualPath(new RequestContext(hc, new RouteData()), values);
+
+ Assert.IsNotNull(vp, "#2");
+ Assert.AreEqual("SomeThing/us-xx", vp.VirtualPath, "#2-1");
+ Assert.AreEqual(r, vp.Route, "#2-2");
+ Assert.AreEqual(0, vp.DataTokens.Count, "#2-3");
+
+ // Case #3: make contry required.
+ r = new MyRoute("{controller}/{country}-{locale}/{action}", new MyRouteHandler())
+ {
+ Defaults = new RouteValueDictionary(new { action = "Index", locale = "en" })
+ };
+ vp = r.GetVirtualPath(new RequestContext(hc, new RouteData()), values);
+
+ Assert.IsNull(vp, "#3");
+ }
+
// Bug #500739
[Test]
public void RouteGetRequiredStringWithDefaults ()