Small refactor of TemplateBinder

We'll need to access the accepted values to do proper link generation, so
separating this process out into 2 parts.

Also moving defaults into the TemplateBinder because they are conceptually
part of the route, not part of the request. I'll do the same for
TemplateMatcher soon, but it's a big change and worth separating.
This commit is contained in:
Ryan Nowak 2014-03-26 18:44:57 -07:00
Родитель 89828f6a92
Коммит cf16d6cba7
4 изменённых файлов: 82 добавлений и 111 удалений

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

@ -12,59 +12,38 @@ namespace Microsoft.AspNet.Routing.Template
{ {
public class TemplateBinder public class TemplateBinder
{ {
public TemplateBinder(Template template) private readonly IDictionary<string, object> _defaults;
private readonly Template _template;
public TemplateBinder(Template template, IDictionary<string, object> defaults)
{ {
if (template == null) if (template == null)
{ {
throw new ArgumentNullException("template"); throw new ArgumentNullException("template");
} }
Template = template; _template = template;
} _defaults = defaults;
public Template Template { get; private set; }
public string Bind(IDictionary<string, object> defaults, IDictionary<string, object> ambientValues, IDictionary<string, object> values)
{
if (values == null)
{
throw new ArgumentNullException("values");
}
var context = GetAcceptedValues(defaults, ambientValues, values);
if (context == null)
{
// We couldn't get values for all the required parameters
return null;
}
return BindValues(context);
} }
// Step 1: Get the list of values we're going to try to use to match and generate this URI // Step 1: Get the list of values we're going to try to use to match and generate this URI
private TemplateBindingContext GetAcceptedValues(IDictionary<string, object> defaults, IDictionary<string, object> ambientValues, IDictionary<string, object> values) public IDictionary<string, object> GetAcceptedValues(IDictionary<string, object> ambientValues, IDictionary<string, object> values)
{ {
Contract.Assert(values != null); var context = new TemplateBindingContext(_defaults, values);
var context = new TemplateBindingContext(defaults, values);
// Find out which entries in the URI are valid for the URI we want to generate. // 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 // 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 // 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>. // values should then be a="1", b="9", c=<no value>.
for (var i = 0; i < Template.Parameters.Count; i++) for (var i = 0; i < _template.Parameters.Count; i++)
{ {
var parameter = Template.Parameters[i]; var parameter = _template.Parameters[i];
// If it's a parameter subsegment, examine the current value to see if it matches the new value // If it's a parameter subsegment, examine the current value to see if it matches the new value
var parameterName = parameter.Name; var parameterName = parameter.Name;
object newParameterValue; object newParameterValue;
var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
if (hasNewParameterValue)
{
context.Use(parameterName);
}
object currentParameterValue = null; object currentParameterValue = null;
var hasCurrentParameterValue = ambientValues != null && ambientValues.TryGetValue(parameterName, out currentParameterValue); var hasCurrentParameterValue = ambientValues != null && ambientValues.TryGetValue(parameterName, out currentParameterValue);
@ -104,23 +83,10 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
// Add all current values that aren't in the URI at all
if (ambientValues != null)
{
foreach (var kvp in ambientValues)
{
var parameter = GetParameter(kvp.Key);
if (parameter == null)
{
context.Accept(kvp.Key, kvp.Value);
}
}
}
// Accept all remaining default values if they match a required parameter // Accept all remaining default values if they match a required parameter
for (int i = 0; i < Template.Parameters.Count; i++) for (int i = 0; i < _template.Parameters.Count; i++)
{ {
var parameter = Template.Parameters[i]; var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll) if (parameter.IsOptional || parameter.IsCatchAll)
{ {
continue; continue;
@ -136,9 +102,9 @@ namespace Microsoft.AspNet.Routing.Template
} }
// Validate that all required parameters have a value. // Validate that all required parameters have a value.
for (var i = 0; i < Template.Parameters.Count; i++) for (var i = 0; i < _template.Parameters.Count; i++)
{ {
var parameter = Template.Parameters[i]; var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll) if (parameter.IsOptional || parameter.IsCatchAll)
{ {
continue; continue;
@ -166,11 +132,7 @@ namespace Microsoft.AspNet.Routing.Template
object value; object value;
if (values.TryGetValue(filter.Key, out value)) if (values.TryGetValue(filter.Key, out value))
{ {
if (RoutePartsEqual(value, filter.Value)) if (!RoutePartsEqual(value, filter.Value))
{
context.Use(filter.Key);
}
else
{ {
// If there is a non-parameterized value in the route and there is a // 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. // new value for it and it doesn't match, this route won't match.
@ -180,20 +142,20 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
return context; return context.AcceptedValues;
} }
// Step 2: If the route is a match generate the appropriate URI // Step 2: If the route is a match generate the appropriate URI
private string BindValues(TemplateBindingContext bindingContext) public string BindValues(IDictionary<string, object> acceptedValues)
{ {
var context = new UriBuildingContext(); var context = new UriBuildingContext();
for (var i = 0; i < Template.Segments.Count; i++) for (var i = 0; i < _template.Segments.Count; i++)
{ {
Contract.Assert(context.BufferState == SegmentState.Beginning); Contract.Assert(context.BufferState == SegmentState.Beginning);
Contract.Assert(context.UriState == SegmentState.Beginning); Contract.Assert(context.UriState == SegmentState.Beginning);
var segment = Template.Segments[i]; var segment = _template.Segments[i];
for (var j = 0; j < segment.Parts.Count; j++) for (var j = 0; j < segment.Parts.Count; j++)
{ {
@ -210,14 +172,24 @@ namespace Microsoft.AspNet.Routing.Template
{ {
// If it's a parameter, get its value // If it's a parameter, get its value
object value; object value;
var hasValue = bindingContext.AcceptedValues.TryGetValue(part.Name, out value); var hasValue = acceptedValues.TryGetValue(part.Name, out value);
if (hasValue) if (hasValue)
{ {
bindingContext.Use(part.Name); acceptedValues.Remove(part.Name);
}
bool isSameAsDefault = false;
object defaultValue;
if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue))
{
if (RoutePartsEqual(value, defaultValue))
{
isSameAsDefault = true;
}
} }
var converted = Convert.ToString(value, CultureInfo.InvariantCulture); var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (bindingContext.AcceptedDefaultValues.Contains(part.Name)) if (isSameAsDefault)
{ {
// If the accepted value is the same as the default value buffer it since // If the accepted value is the same as the default value buffer it since
// we won't necessarily add it to the URI we generate. // we won't necessarily add it to the URI we generate.
@ -243,10 +215,16 @@ namespace Microsoft.AspNet.Routing.Template
var encoded = new StringBuilder(); var encoded = new StringBuilder();
encoded.Append(UriEncode(context.Build())); encoded.Append(UriEncode(context.Build()));
// Generate the query string // Generate the query string from the remaining values
var firstParam = true; var firstParam = true;
foreach (var kvp in bindingContext.UnusedValues) foreach (var kvp in acceptedValues)
{ {
if (_defaults != null && _defaults.ContainsKey(kvp.Key))
{
// This value is a 'filter' we don't need to put it in the query string.
continue;
}
var converted = Convert.ToString(kvp.Value, CultureInfo.InvariantCulture); var converted = Convert.ToString(kvp.Value, CultureInfo.InvariantCulture);
if (String.IsNullOrEmpty(converted)) if (String.IsNullOrEmpty(converted))
{ {
@ -277,9 +255,9 @@ namespace Microsoft.AspNet.Routing.Template
private TemplatePart GetParameter(string name) private TemplatePart GetParameter(string name)
{ {
for (int i = 0; i < Template.Parameters.Count; i++) for (int i = 0; i < _template.Parameters.Count; i++)
{ {
var parameter = Template.Parameters[i]; var parameter = _template.Parameters[i];
if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
{ {
return parameter; return parameter;
@ -333,8 +311,6 @@ namespace Microsoft.AspNet.Routing.Template
private readonly IDictionary<string, object> _defaults; private readonly IDictionary<string, object> _defaults;
private readonly Dictionary<string, object> _acceptedValues; private readonly Dictionary<string, object> _acceptedValues;
private readonly HashSet<string> _acceptedDefaultValues;
private readonly Dictionary<string, object> _unusedValues;
private readonly Dictionary<string, object> _filters; private readonly Dictionary<string, object> _filters;
public TemplateBindingContext(IDictionary<string, object> defaults, IDictionary<string, object> values) public TemplateBindingContext(IDictionary<string, object> defaults, IDictionary<string, object> values)
@ -347,8 +323,6 @@ namespace Microsoft.AspNet.Routing.Template
_defaults = defaults; _defaults = defaults;
_acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); _acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
_acceptedDefaultValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_unusedValues = new Dictionary<string, object>(values, StringComparer.OrdinalIgnoreCase);
if (_defaults != null) if (_defaults != null)
{ {
@ -361,20 +335,6 @@ namespace Microsoft.AspNet.Routing.Template
get { return _acceptedValues; } get { return _acceptedValues; }
} }
/// <remarks>
/// These are values that are equivalent to the default. These aren't written to the url unless
/// necessary.
/// </remarks>>
public HashSet<string> AcceptedDefaultValues
{
get { return _acceptedDefaultValues; }
}
public Dictionary<string, object> UnusedValues
{
get { return _unusedValues; }
}
public Dictionary<string, object> Filters public Dictionary<string, object> Filters
{ {
get { return _filters; } get { return _filters; }
@ -385,15 +345,6 @@ namespace Microsoft.AspNet.Routing.Template
if (!_acceptedValues.ContainsKey(key)) if (!_acceptedValues.ContainsKey(key))
{ {
_acceptedValues.Add(key, value); _acceptedValues.Add(key, value);
object defaultValue;
if (_defaults != null && _defaults.TryGetValue(key, out defaultValue))
{
if (RoutePartsEqual(value, defaultValue))
{
_acceptedDefaultValues.Add(key);
}
}
} }
} }
@ -406,8 +357,6 @@ namespace Microsoft.AspNet.Routing.Template
{ {
_filters.Remove(key); _filters.Remove(key);
_acceptedValues.Add(key, value); _acceptedValues.Add(key, value);
_acceptedDefaultValues.Add(key);
} }
} }
@ -416,11 +365,6 @@ namespace Microsoft.AspNet.Routing.Template
return !_acceptedValues.ContainsKey(key); return !_acceptedValues.ContainsKey(key);
} }
public void Use(string key)
{
_unusedValues.Remove(key);
}
private string DebuggerToString() private string DebuggerToString()
{ {
return string.Format( return string.Format(

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

@ -37,7 +37,7 @@ namespace Microsoft.AspNet.Routing.Template
_parsedTemplate = TemplateParser.Parse(RouteTemplate); _parsedTemplate = TemplateParser.Parse(RouteTemplate);
_matcher = new TemplateMatcher(_parsedTemplate); _matcher = new TemplateMatcher(_parsedTemplate);
_binder = new TemplateBinder(_parsedTemplate); _binder = new TemplateBinder(_parsedTemplate, _defaults);
} }
public IDictionary<string, object> Defaults public IDictionary<string, object> Defaults
@ -80,6 +80,13 @@ namespace Microsoft.AspNet.Routing.Template
public string GetVirtualPath(VirtualPathContext context) public string GetVirtualPath(VirtualPathContext context)
{ {
var values = _binder.GetAcceptedValues(context.AmbientValues, context.Values);
if (values == null)
{
// We're missing one the required values for this route.
return null;
}
// Validate that the target can accept these values. // Validate that the target can accept these values.
var path = _target.GetVirtualPath(context); var path = _target.GetVirtualPath(context);
if (path != null) if (path != null)
@ -93,9 +100,7 @@ namespace Microsoft.AspNet.Routing.Template
return null; return null;
} }
// This could be optimized more heavily - right now we try to do the full url path = _binder.BindValues(values);
// generation after validating, but we could do it in two phases if the perf is better.
path = _binder.Bind(_defaults, context.AmbientValues, context.Values);
if (path == null) if (path == null)
{ {
context.IsBound = false; context.IsBound = false;

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

@ -125,12 +125,23 @@ namespace Microsoft.AspNet.Routing.Template.Tests
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template)); var binder = new TemplateBinder(TemplateParser.Parse(template), defaults);
// Act // Act & Assert
var boundTemplate = binder.Bind(defaults, null, values); var acceptedValues = binder.GetAcceptedValues(null, values);
if (acceptedValues == null)
{
if (expected == null)
{
return;
}
else
{
Assert.NotNull(acceptedValues);
}
}
// Assert var boundTemplate = binder.BindValues(acceptedValues);
if (expected == null) if (expected == null)
{ {
Assert.Null(boundTemplate); Assert.Null(boundTemplate);
@ -947,12 +958,23 @@ namespace Microsoft.AspNet.Routing.Template.Tests
string expected) string expected)
{ {
// Arrange // Arrange
var binder = new TemplateBinder(TemplateParser.Parse(template)); var binder = new TemplateBinder(TemplateParser.Parse(template), defaults);
// Act // Act & Assert
var boundTemplate = binder.Bind(defaults, ambientValues, values); var acceptedValues = binder.GetAcceptedValues(ambientValues, values);
if (acceptedValues == null)
{
if (expected == null)
{
return;
}
else
{
Assert.NotNull(acceptedValues);
}
}
// Assert var boundTemplate = binder.BindValues(acceptedValues);
if (expected == null) if (expected == null)
{ {
Assert.Null(boundTemplate); Assert.Null(boundTemplate);

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

@ -23,7 +23,7 @@
}, },
"net45": { "net45": {
"dependencies": { "dependencies": {
"Moq": "4.2.1402.2112", "Moq": "4.2.1312.1622",
"System.Runtime": "" "System.Runtime": ""
} }
} }