Fix for issue 85 - Dictionary types should return null on key not found

This change makes RouteValueDictionary a full IDictionary implementation
instead of a subclass of Dictionary.

Followed the patterns used in the old implementation, namely preserving
the struct-returning behavior of Keys/Values/GetEnumerator.
This commit is contained in:
Ryan Nowak 2014-07-23 12:04:41 -07:00
parent 3eb6c22330
commit ae65001e84
5 changed files with 215 additions and 22 deletions

View File

@ -2,21 +2,48 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
namespace Microsoft.AspNet.Routing namespace Microsoft.AspNet.Routing
{ {
public class RouteValueDictionary : Dictionary<string, object> /// <summary>
/// An <see cref="IDictionary{string, object}"/> type for route values.
/// </summary>
public class RouteValueDictionary : IDictionary<string, object>
{ {
private readonly Dictionary<string, object> _dictionary;
/// <summary>
/// Creates an empty RouteValueDictionary.
/// </summary>
public RouteValueDictionary() public RouteValueDictionary()
: base(StringComparer.OrdinalIgnoreCase)
{ {
_dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
} }
/// <summary>
/// Creates a RouteValueDictionary initialized with the provided input values.
/// </summary>
/// <param name="values">Input values to copy into the dictionary.</param>
public RouteValueDictionary([NotNull] IDictionary<string, object> values)
{
_dictionary = new Dictionary<string, object>(values, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Creates a RouteValueDictionary initialized with the provided input values.
/// </summary>
/// <param name="values">Input values to copy into the dictionary.</param>
/// <remarks>
/// The input parameter is interpreted as a set of key-value-pairs where the property names
/// are keys, and property values are the values, and copied into the dictionary. Only public
/// instance non-index properties are considered.
/// </remarks>
public RouteValueDictionary(object obj) public RouteValueDictionary(object obj)
: base(StringComparer.OrdinalIgnoreCase) : this()
{ {
if (obj != null) if (obj != null)
{ {
@ -46,9 +73,162 @@ namespace Microsoft.AspNet.Routing
} }
} }
public RouteValueDictionary(IDictionary<string, object> other) /// <inheritdoc />
: base(other, StringComparer.OrdinalIgnoreCase) public object this[[NotNull] string key]
{ {
get
{
object value;
_dictionary.TryGetValue(key, out value);
return value;
}
set
{
_dictionary[key] = value;
}
}
/// <summary>
/// Gets the comparer for this dictionary.
/// </summary>
/// <remarks>
/// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
/// </remarks>
public IEqualityComparer<string> Comparer
{
get
{
return _dictionary.Comparer;
}
}
/// <inheritdoc />
public int Count
{
get
{
return _dictionary.Count;
}
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{
get
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).IsReadOnly;
}
}
/// <inheritdoc />
public Dictionary<string, object>.KeyCollection Keys
{
get
{
return _dictionary.Keys;
}
}
/// <inheritdoc />
ICollection<string> IDictionary<string, object>.Keys
{
get
{
return _dictionary.Keys;
}
}
/// <inheritdoc />
public Dictionary<string, object>.ValueCollection Values
{
get
{
return _dictionary.Values;
}
}
/// <inheritdoc />
ICollection<object> IDictionary<string, object>.Values
{
get
{
return _dictionary.Values;
}
}
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{
((ICollection<KeyValuePair<string, object>>)_dictionary).Add(item);
}
/// <inheritdoc />
public void Add([NotNull] string key, object value)
{
_dictionary.Add(key, value);
}
/// <inheritdoc />
public void Clear()
{
_dictionary.Clear();
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).Contains(item);
}
/// <inheritdoc />
public bool ContainsKey([NotNull] string key)
{
return _dictionary.ContainsKey(key);
}
/// <inheritdoc />
void ICollection<KeyValuePair<string, object>>.CopyTo(
[NotNull] KeyValuePair<string, object>[] array,
int arrayIndex)
{
((ICollection<KeyValuePair<string, object>>)_dictionary).CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public Dictionary<string, object>.Enumerator GetEnumerator()
{
return _dictionary.GetEnumerator();
}
/// <inheritdoc />
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
return _dictionary.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return _dictionary.GetEnumerator();
}
/// <inheritdoc />
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
{
return ((ICollection<KeyValuePair<string, object>>)_dictionary).Remove(item);
}
/// <inheritdoc />
public bool Remove([NotNull] string key)
{
return _dictionary.Remove(key);
}
/// <inheritdoc />
public bool TryGetValue([NotNull] string key, out object value)
{
return _dictionary.TryGetValue(key, out value);
} }
} }
} }

View File

@ -147,7 +147,7 @@ namespace Microsoft.AspNet.Routing.Template
// Add any ambient values that don't match parameters - they need to be visible to constraints // Add any ambient values that don't match parameters - they need to be visible to constraints
// but they will ignored by link generation. // but they will ignored by link generation.
var combinedValues = new Dictionary<string, object>(context.AcceptedValues, StringComparer.OrdinalIgnoreCase); var combinedValues = new RouteValueDictionary(context.AcceptedValues);
if (ambientValues != null) if (ambientValues != null)
{ {
foreach (var kvp in ambientValues) foreach (var kvp in ambientValues)
@ -292,7 +292,6 @@ namespace Microsoft.AspNet.Routing.Template
return null; return null;
} }
/// <summary> /// <summary>
/// Compares two objects for equality as parts of a case-insensitive path. /// Compares two objects for equality as parts of a case-insensitive path.
/// </summary> /// </summary>
@ -342,8 +341,8 @@ 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 RouteValueDictionary _acceptedValues;
private readonly Dictionary<string, object> _filters; private readonly RouteValueDictionary _filters;
public TemplateBindingContext(IDictionary<string, object> defaults, IDictionary<string, object> values) public TemplateBindingContext(IDictionary<string, object> defaults, IDictionary<string, object> values)
{ {
@ -354,20 +353,20 @@ namespace Microsoft.AspNet.Routing.Template
_defaults = defaults; _defaults = defaults;
_acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); _acceptedValues = new RouteValueDictionary();
if (_defaults != null) if (_defaults != null)
{ {
_filters = new Dictionary<string, object>(defaults, StringComparer.OrdinalIgnoreCase); _filters = new RouteValueDictionary(defaults);
} }
} }
public Dictionary<string, object> AcceptedValues public RouteValueDictionary AcceptedValues
{ {
get { return _acceptedValues; } get { return _acceptedValues; }
} }
public Dictionary<string, object> Filters public RouteValueDictionary Filters
{ {
get { return _filters; } get { return _filters; }
} }

View File

@ -32,10 +32,10 @@ namespace Microsoft.AspNet.Routing.Template
if (defaults == null) if (defaults == null)
{ {
defaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); defaults = new RouteValueDictionary();
} }
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); var values = new RouteValueDictionary();
for (var i = 0; i < requestSegments.Length; i++) for (var i = 0; i < requestSegments.Length; i++)
{ {
@ -175,7 +175,7 @@ namespace Microsoft.AspNet.Routing.Template
private bool MatchComplexSegment(TemplateSegment routeSegment, private bool MatchComplexSegment(TemplateSegment routeSegment,
string requestSegment, string requestSegment,
IDictionary<string, object> defaults, IDictionary<string, object> defaults,
Dictionary<string, object> values) RouteValueDictionary values)
{ {
Contract.Assert(routeSegment != null); Contract.Assert(routeSegment != null);
Contract.Assert(routeSegment.Parts.Count > 1); Contract.Assert(routeSegment.Parts.Count > 1);

View File

@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Routing.Template
_target = target; _target = target;
_routeTemplate = routeTemplate ?? string.Empty; _routeTemplate = routeTemplate ?? string.Empty;
Name = routeName; Name = routeName;
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); _defaults = defaults ?? new RouteValueDictionary();
_constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate) ?? _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate) ??
new Dictionary<string, IRouteConstraint>(); new Dictionary<string, IRouteConstraint>();

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases. // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases.
[Fact] [Fact]
public async void Match_Success_LeadingSlash() public async Task Match_Success_LeadingSlash()
{ {
// Arrange // Arrange
var route = CreateRoute("{controller}/{action}"); var route = CreateRoute("{controller}/{action}");
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Fact] [Fact]
public async void Match_Success_RootUrl() public async Task Match_Success_RootUrl()
{ {
// Arrange // Arrange
var route = CreateRoute(""); var route = CreateRoute("");
@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Fact] [Fact]
public async void Match_Success_Defaults() public async Task Match_Success_Defaults()
{ {
// Arrange // Arrange
var route = CreateRoute("{controller}/{action}", new { action = "Index" }); var route = CreateRoute("{controller}/{action}", new { action = "Index" });
@ -72,7 +72,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Fact] [Fact]
public async void Match_Fails() public async Task Match_Fails()
{ {
// Arrange // Arrange
var route = CreateRoute("{controller}/{action}"); var route = CreateRoute("{controller}/{action}");
@ -86,7 +86,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
} }
[Fact] [Fact]
public async void Match_RejectedByHandler() public async Task Match_RejectedByHandler()
{ {
// Arrange // Arrange
var route = CreateRoute("{controller}", accept: false); var route = CreateRoute("{controller}", accept: false);
@ -102,6 +102,20 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.NotNull(context.RouteData.Values); Assert.NotNull(context.RouteData.Values);
} }
[Fact]
public async Task Match_RouteValuesDoesntThrowOnKeyNotFound()
{
// Arrange
var route = CreateRoute("{controller}/{action}");
var context = CreateRouteContext("/Home/Index");
// Act
await route.RouteAsync(context);
// Assert
Assert.Null(context.RouteData.Values["1controller"]);
}
private static RouteContext CreateRouteContext(string requestPath) private static RouteContext CreateRouteContext(string requestPath)
{ {
var request = new Mock<HttpRequest>(MockBehavior.Strict); var request = new Mock<HttpRequest>(MockBehavior.Strict);