| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721 |
- //
- // PatternParser.cs
- //
- // Author:
- // Atsushi Enomoto <[email protected]>
- // Marek Habersack <[email protected]>
- //
- // Copyright (C) 2008-2010 Novell Inc. http://novell.com
- //
- //
- // Permission is hereby granted, free of charge, to any person obtaining
- // a copy of this software and associated documentation files (the
- // "Software"), to deal in the Software without restriction, including
- // without limitation the rights to use, copy, modify, merge, publish,
- // distribute, sublicense, and/or sell copies of the Software, and to
- // permit persons to whom the Software is furnished to do so, subject to
- // the following conditions:
- //
- // The above copyright notice and this permission notice shall be
- // included in all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- //
- 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
- {
- sealed class PatternParser
- {
- struct PatternSegment
- {
- public bool AllLiteral;
- public List <PatternToken> Tokens;
- }
-
- static readonly char[] placeholderDelimiters = { '{', '}' };
-
- PatternSegment[] segments;
- Dictionary <string, bool> parameterNames;
- PatternToken[] tokens;
-
- int segmentCount;
- bool haveSegmentWithCatchAll;
-
- public string Url {
- get;
- private set;
- }
-
- public PatternParser (string pattern)
- {
- this.Url = pattern;
- Parse ();
- }
- void Parse ()
- {
- string url = Url;
- parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
-
- if (!String.IsNullOrEmpty (url)) {
- if (url [0] == '~' || url [0] == '/')
- throw new ArgumentException ("Url must not start with '~' or '/'");
- if (url.IndexOf ('?') >= 0)
- throw new ArgumentException ("Url must not contain '?'");
- } else {
- segments = new PatternSegment [0];
- tokens = new PatternToken [0];
- return;
- }
-
- string[] parts = url.Split ('/');
- int partsCount = segmentCount = parts.Length;
- var allTokens = new List <PatternToken> ();
- PatternToken tmpToken;
-
- segments = new PatternSegment [partsCount];
-
- for (int i = 0; i < partsCount; i++) {
- if (haveSegmentWithCatchAll)
- throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
-
- int catchAlls = 0;
- string part = parts [i];
- int partLength = part.Length;
- var tokens = new List <PatternToken> ();
- if (partLength == 0 && i < partsCount - 1)
- throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
- if (part.IndexOf ("{}") != -1)
- throw new ArgumentException ("Empty URL parameter name is not allowed");
- if (i > 0)
- allTokens.Add (null);
-
- if (part.IndexOfAny (placeholderDelimiters) == -1) {
- // no placeholders here, short-circuit it
- tmpToken = new PatternToken (PatternTokenType.Literal, part);
- tokens.Add (tmpToken);
- allTokens.Add (tmpToken);
- segments [i].AllLiteral = true;
- segments [i].Tokens = tokens;
- continue;
- }
- string tmp;
- int from = 0, start;
- bool allLiteral = true;
- while (from < partLength) {
- start = part.IndexOf ('{', from);
- if (start >= partLength - 2)
- throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
- if (start < 0) {
- if (part.IndexOf ('}', from) >= from)
- throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
- tmp = part.Substring (from);
- tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
- tokens.Add (tmpToken);
- allTokens.Add (tmpToken);
- from += tmp.Length;
- break;
- }
- if (from == 0 && start > 0) {
- tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
- tokens.Add (tmpToken);
- allTokens.Add (tmpToken);
- }
-
- int end = part.IndexOf ('}', start + 1);
- int next = part.IndexOf ('{', start + 1);
-
- if (end < 0 || next >= 0 && next < end)
- throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
- if (next == end + 1)
- throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
- if (next == -1)
- next = partLength;
-
- string token = part.Substring (start + 1, end - start - 1);
- PatternTokenType type;
- if (token [0] == '*') {
- catchAlls++;
- haveSegmentWithCatchAll = true;
- type = PatternTokenType.CatchAll;
- token = token.Substring (1);
- } else
- type = PatternTokenType.Standard;
- if (!parameterNames.ContainsKey (token))
- parameterNames.Add (token, true);
- tmpToken = new PatternToken (type, token);
- tokens.Add (tmpToken);
- allTokens.Add (tmpToken);
- allLiteral = false;
-
- if (end < partLength - 1) {
- token = part.Substring (end + 1, next - end - 1);
- tmpToken = new PatternToken (PatternTokenType.Literal, token);
- tokens.Add (tmpToken);
- allTokens.Add (tmpToken);
- end += token.Length;
- }
- if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
- throw new ArgumentException ("A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.");
- from = end + 1;
- }
-
- segments [i].AllLiteral = allLiteral;
- segments [i].Tokens = tokens;
- }
- if (allTokens.Count > 0)
- this.tokens = allTokens.ToArray ();
- allTokens = null;
- }
- RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
- {
- if (defaults != null && defaults.Count > 0) {
- string key;
- foreach (var def in defaults) {
- key = def.Key;
- if (dict.ContainsKey (key))
- continue;
- dict.Add (key, def.Value);
- }
- }
- 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];
- int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
- int startIndex = pathSegmentLength - 1;
- PatternTokenType tokenType;
- int tokensCount = tokens.Count;
- PatternToken token;
- string tokenName;
- for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
- token = tokens [tokenIndex];
- if (startIndex < 0)
- return false;
- tokenType = token.Type;
- tokenName = token.Name;
- if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
- var sb = new StringBuilder ();
- for (int j = segIndex; j < argsCount; j++) {
- if (j > segIndex)
- sb.Append ('/');
- sb.Append (argSegs [j]);
- }
-
- ret.Add (tokenName, sb.ToString ());
- break;
- }
- int scanIndex;
- if (token.Type == PatternTokenType.Literal) {
- int nameLen = tokenName.Length;
- if (startIndex + 1 < nameLen)
- return false;
- scanIndex = startIndex - nameLen + 1;
- if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
- return false;
- startIndex = scanIndex - 1;
- continue;
- }
- // Standard token
- int nextTokenIndex = tokenIndex - 1;
- if (nextTokenIndex < 0) {
- // First token
- ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
- continue;
- }
- if (startIndex == 0)
- return false;
-
- var nextToken = tokens [nextTokenIndex];
- string nextTokenName = nextToken.Name;
- // Skip one char, since there can be no empty segments and if the
- // current token's value happens to be the same as preceeding
- // literal text, we'll save some time and complexity.
- scanIndex = startIndex - 1;
- int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
- if (lastIndex == -1)
- return false;
- lastIndex += nextTokenName.Length - 1;
-
- string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
- if (String.IsNullOrEmpty (sectionValue))
- return false;
- ret.Add (tokenName, sectionValue);
- startIndex = lastIndex;
- }
-
- return true;
- }
- public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
- {
- var ret = new RouteValueDictionary ();
- string url = Url;
- string [] argSegs;
- int argsCount;
-
- if (String.IsNullOrEmpty (path)) {
- argSegs = null;
- argsCount = 0;
- } else {
- // quick check
- if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
- return AddDefaults (ret, defaults);
- argSegs = path.Split ('/');
- argsCount = argSegs.Length;
- if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
- argsCount--; // path ends with a trailinig '/'
- }
- bool haveDefaults = defaults != null && defaults.Count > 0;
- if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
- argsCount = 0;
-
- if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
- return null;
- int i = 0;
- foreach (PatternSegment segment in segments) {
- if (i >= argsCount)
- break;
-
- if (segment.AllLiteral) {
- if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
- return null;
- i++;
- continue;
- }
- if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
- return null;
- i++;
- }
- // Check the remaining segments, if any, and see if they are required
- //
- // If a segment has more than one section (i.e. there's at least one
- // literal, then it cannot match defaults
- //
- // All of the remaining segments must have all defaults provided and they
- // must not be literals or the match will fail.
- if (i < segmentCount) {
- if (!haveDefaults)
- return null;
-
- for (;i < segmentCount; i++) {
- var segment = segments [i];
- if (segment.AllLiteral)
- return null;
-
- var tokens = segment.Tokens;
- if (tokens.Count != 1)
- return null;
- // if token is catch-all, we're done.
- if (tokens [0].Type == PatternTokenType.CatchAll)
- break;
- if (!defaults.ContainsKey (tokens [0].Name))
- return null;
- }
- } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
- return null;
-
- return AddDefaults (ret, defaults);
- }
-
- public string BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, RouteValueDictionary constraints, out RouteValueDictionary usedValues)
- {
- usedValues = null;
- if (requestContext == null)
- return null;
- RouteData routeData = requestContext.RouteData;
- 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;
- }
- }
- // 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);
- }
- }
- }
- // 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);
- }
- }
- // 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);
- }
- }
- // 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);
- }
- }
- // 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;
- }
- }
- // 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);
- }
- 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;
- }
- }
- }
- #endregion
- #region Step 2: If the route is a match generate the appropriate URL
- var uri = new StringBuilder ();
- var pendingParts = new StringBuilder ();
- var pendingPartsAreAllSafe = false;
- bool blockAllUriAppends = false;
- var allSegments = new List<PatternSegment?> ();
- // 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);
- }
- // 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;
- }
- }
- 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 || token.Type == PatternTokenType.CatchAll) {
- 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");
- }
- }
- }
- 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 URL
- uri.Append (pendingParts.ToString ());
- pendingParts.Length = 0;
- }
- }
- }
- }
- 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
- 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)));
- }
- }
- }
- #endregion
- usedValues = acceptedValues;
- return uri.ToString();
- }
- }
- }
|