removing datatokens/constraints/url-generation

This commit is contained in:
Ryan Nowak 2014-02-06 11:56:48 -08:00
Родитель d4904e8701
Коммит 85225055b9
8 изменённых файлов: 73 добавлений и 2421 удалений

Просмотреть файл

@ -1,16 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNet.Routing.Template
{
/// <summary>
/// Represents a URI generated from a <see cref="TemplateParsedRoute"/>.
/// </summary>
public class BoundRouteTemplate
{
public string BoundTemplate { get; set; }
public IDictionary<string, object> Values { get; set; }
}
}

Просмотреть файл

@ -1,12 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Routing.Template
{
public interface ITemplateRouteConstraint
{
bool Match(HttpContext context, IRoute route, string parameterName, IDictionary<string, object> values, RouteDirection routeDirection);
}
}

Просмотреть файл

@ -1,11 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing.Template
{
public interface IVirtualPathData
{
IRoute Route { get; }
string VirtualPath { get; set; }
}
}

Просмотреть файл

@ -1,10 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing.Template
{
public enum RouteDirection
{
UriResolution = 0,
UriGeneration
}
}

Просмотреть файл

@ -2,16 +2,12 @@
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 class TemplateParsedRoute
{
public TemplateParsedRoute(IList<PathSegment> pathSegments)
{
@ -21,392 +17,6 @@ namespace Microsoft.AspNet.Routing.Template
internal IList<PathSegment> 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<string, object> currentValues, IDictionary<string, object> values, IDictionary<string, object> defaultValues, IDictionary<string, object> constraints)
{
if (currentValues == null)
{
currentValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
if (values == null)
{
values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
if (defaultValues == null)
{
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
// The set of values we should be using when generating the URI in this route
IDictionary<string, object> acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// Keep track of which new values have been used
HashSet<string> unusedNewValues = new HashSet<string>(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=<no value>.
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<string, object> otherDefaultValues = new Dictionary<string, object>(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<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> action)
{
for (int i = 0; i < pathSegments.Count; i++)
@ -458,47 +68,6 @@ namespace Microsoft.AspNet.Routing.Template
return true;
}
private static PathParameterSubsegment GetParameterSubsegment(IList<PathSegment> 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<string, object> 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<string, object> Match(string virtualPath, IDictionary<string, object> defaultValues)
{
IList<string> requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath);
@ -808,35 +377,5 @@ namespace Microsoft.AspNet.Routing.Template
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));
}
}
}

Просмотреть файл

@ -2,68 +2,34 @@
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
{
/// <summary>
/// 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.
/// </summary>
public class TemplateRoute : IRoute
{
/// <summary>
/// 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.
/// </summary>
public static readonly string HttpRouteKey = "httproute";
private readonly IDictionary<string, object> _defaults;
private readonly IRouteEndpoint _endpoint;
private readonly TemplateParsedRoute _parsedRoute;
private readonly string _routeTemplate;
private string _routeTemplate;
private IDictionary<string, object> _defaults;
private IDictionary<string, object> _constraints;
private IDictionary<string, object> _dataTokens;
public TemplateRoute()
: this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null)
public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate)
: this(endpoint, routeTemplate, null)
{
}
public TemplateRoute(string routeTemplate)
: this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null)
public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate, IDictionary<string, object> defaults)
{
}
if (endpoint == null)
{
throw new ArgumentNullException("endpoint");
}
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults)
: this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null)
{
}
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints)
: this(routeTemplate, defaults, constraints, dataTokens: null, handler: null)
{
}
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens)
: this(routeTemplate, defaults, constraints, dataTokens, handler: null)
{
}
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IRouteEndpoint handler)
{
_endpoint = endpoint;
_routeTemplate = routeTemplate == null ? String.Empty : routeTemplate;
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
_constraints = constraints ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
_dataTokens = dataTokens ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Handler = handler;
// The parser will throw for invalid routes.
ParsedRoute = TemplateRouteParser.Parse(RouteTemplate);
_parsedRoute = TemplateRouteParser.Parse(RouteTemplate);
}
public IDictionary<string, object> Defaults
@ -71,159 +37,39 @@ namespace Microsoft.AspNet.Routing.Template
get { return _defaults; }
}
public IDictionary<string, object> Constraints
public IRouteEndpoint Endpoint
{
get { return _constraints; }
get { return _endpoint; }
}
public IDictionary<string, object> 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)
public virtual RouteMatch Match(RouteContext context)
{
if (request == null)
if (context == null)
{
throw new ArgumentNullException("request");
throw new ArgumentNullException("context");
}
var requestPath = request.Request.Path.Value;
var requestPath = context.RequestPath;
if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
requestPath = requestPath.Substring(1);
}
IDictionary<string, object> values = ParsedRoute.Match(requestPath, _defaults);
IDictionary<string, object> 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))
else
{
return null;
return new RouteMatch(_endpoint, values);
}
return new RouteMatch(null, values);
}
/// <summary>
/// Attempt to generate a URI that represents the values passed in based on current
/// values from the <see cref="HttpRouteData"/> and new values using the specified <see cref="TemplateRoute"/>.
/// </summary>
/// <param name="request">The HTTP request message.</param>
/// <param name="values">The route values.</param>
/// <returns>A <see cref="VirtualPathData"/> instance or null if URI cannot be generated.</returns>
public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary<string, object> 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<IRouteValues>();
IDictionary<string, object> 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<string, object> GetRouteDictionaryWithoutHttpRouteKey(IDictionary<string, object> routeValues)
{
var newRouteValues = new Dictionary<string, object>(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<string, object> 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<string, object> values, RouteDirection routeDirection)
{
if (Constraints != null)
{
foreach (KeyValuePair<string, object> constraintsItem in Constraints)
{
if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection))
{
return false;
}
}
}
return true;
}
public RouteMatch Match(RouteContext context)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -1,42 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Routing.Template
{
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;
}
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу