Add support for model binding dictionaries from `prefix[name]=value` entries

- #1418
- add new fallback binding in `DictionaryModelBinder`
 - similar to MVC 5 approach but more explicit and with better key conversion support
- fix bugs in `PrefixContainer` encountered while adding new tests of #1418 scenarios
 - did not handle entries like "[key]" or "prefix.key[index]" correctly
 - refactor part of `GetKeyFromEmptyPrefix()` into `IndexOfDelimiter()`; share with `GetKeyFromNonEmptyPrefix()`
 - extend `ReadableStringCollectionValueProviderTest` to cover bracketed key segments

nits:
- remove use of "foo", "bar", and "baz" in affected test classes
- `""` -> `string.Empty`
- `vpResult` -> `result`
This commit is contained in:
Doug Bunting 2015-07-19 22:05:58 -07:00
parent 6033679193
commit 79a2982441
6 changed files with 565 additions and 128 deletions

View File

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -13,15 +16,88 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <typeparam name="TValue">Type of values in the dictionary.</typeparam>
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
{
/// <inheritdoc />
public override async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
var result = await base.BindModelAsync(bindingContext);
if (result == null || !result.IsModelSet)
{
// No match for the prefix at all.
return result;
}
Debug.Assert(result.Model != null);
var model = (Dictionary<TKey, TValue>)result.Model;
if (model.Count != 0)
{
// ICollection<KeyValuePair<TKey, TValue>> approach was successful.
return result;
}
var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
if (enumerableValueProvider == null)
{
// No IEnumerableValueProvider available for the fallback approach. For example the user may have
// replaced the ValueProvider with something other than a CompositeValueProvider.
return result;
}
// Attempt to bind dictionary from a set of prefix[key]=value entries. Get the short and long keys first.
var keys = await enumerableValueProvider.GetKeysFromPrefixAsync(bindingContext.ModelName);
if (!keys.Any())
{
// No entries with the expected keys.
return result;
}
// Update the existing successful but empty ModelBindingResult.
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
var valueMetadata = metadataProvider.GetMetadataForType(typeof(TValue));
var valueBindingContext = ModelBindingContext.GetChildModelBindingContext(
bindingContext,
bindingContext.ModelName,
valueMetadata);
var modelBinder = bindingContext.OperationBindingContext.ModelBinder;
var validationNode = result.ValidationNode;
foreach (var key in keys)
{
var dictionaryKey = ConvertFromString(key.Key);
valueBindingContext.ModelName = key.Value;
var valueResult = await modelBinder.BindModelAsync(valueBindingContext);
// Always add an entry to the dictionary but validate only if binding was successful.
model[dictionaryKey] = ModelBindingHelper.CastOrDefault<TValue>(valueResult?.Model);
if (valueResult != null && valueResult.IsModelSet)
{
validationNode.ChildNodes.Add(valueResult.ValidationNode);
}
}
return result;
}
/// <inheritdoc />
protected override object GetModel(IEnumerable<KeyValuePair<TKey, TValue>> newCollection)
{
return newCollection?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
/// <inheritdoc />
protected override object CreateEmptyCollection()
{
return new Dictionary<TKey, TValue>();
}
private static TKey ConvertFromString(string keyString)
{
// Use InvariantCulture to convert string since ExpressionHelper.GetExpressionText() used that culture.
var keyResult = new ValueProviderResult(keyString);
var keyObject = keyResult.ConvertTo(typeof(TKey));
return ModelBindingHelper.CastOrDefault<TKey>(keyObject);
}
}
}

View File

@ -81,31 +81,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static void GetKeyFromEmptyPrefix(string entry, IDictionary<string, string> results)
{
var dotPosition = entry.IndexOf('.');
var bracketPosition = entry.IndexOf('[');
var delimiterPosition = -1;
string key;
string fullName;
var delimiterPosition = IndexOfDelimiter(entry, 0);
if (dotPosition == -1)
if (delimiterPosition == 0 && entry[0] == '[')
{
if (bracketPosition != -1)
// Handle an entry such as "[key]".
var bracketPosition = entry.IndexOf(']', 1);
if (bracketPosition == -1)
{
delimiterPosition = bracketPosition;
// Malformed for dictionary.
return;
}
key = entry.Substring(1, bracketPosition - 1);
fullName = entry.Substring(0, bracketPosition + 1);
}
else
{
if (bracketPosition == -1)
{
delimiterPosition = dotPosition;
}
else
{
delimiterPosition = Math.Min(dotPosition, bracketPosition);
}
// Handle an entry such as "key", "key.property" and "key[index]".
key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition);
fullName = key;
}
var key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition);
results[key] = key;
if (!results.ContainsKey(key))
{
results.Add(key, fullName);
}
}
private static void GetKeyFromNonEmptyPrefix(string prefix, string entry, IDictionary<string, string> results)
@ -117,17 +120,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
switch (entry[prefix.Length])
{
case '.':
var dotPosition = entry.IndexOf('.', keyPosition);
if (dotPosition == -1)
// Handle an entry such as "prefix.key", "prefix.key.property" and "prefix.key[index]".
var delimiterPosition = IndexOfDelimiter(entry, keyPosition);
if (delimiterPosition == -1)
{
dotPosition = entry.Length;
// Neither '.' nor '[' found later in the name. Use rest of the string.
key = entry.Substring(keyPosition);
fullName = entry;
}
else
{
key = entry.Substring(keyPosition, delimiterPosition - keyPosition);
fullName = entry.Substring(0, delimiterPosition);
}
key = entry.Substring(keyPosition, dotPosition - keyPosition);
fullName = entry.Substring(0, dotPosition);
break;
case '[':
// Handle an entry such as "prefix[key]".
var bracketPosition = entry.IndexOf(']', keyPosition);
if (bracketPosition == -1)
{
@ -140,6 +149,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
break;
default:
// Ignore an entry such as "prefixA".
return;
}
@ -188,6 +198,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
private static int IndexOfDelimiter(string entry, int startIndex)
{
int delimiterPosition;
var bracketPosition = entry.IndexOf('[', startIndex);
var dotPosition = entry.IndexOf('.', startIndex);
if (dotPosition == -1)
{
delimiterPosition = bracketPosition;
}
else if (bracketPosition == -1)
{
delimiterPosition = dotPosition;
}
else
{
delimiterPosition = Math.Min(dotPosition, bracketPosition);
}
return delimiterPosition;
}
/// <summary>
/// Convert an ICollection to an array, removing null values. Fast path for case where
/// there are no null values.

View File

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DNX451
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Moq;
@ -64,6 +67,200 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal("eighty-four", dictionary[84]);
}
// modelName, keyFormat, dictionary
public static TheoryData<string, string, IDictionary<string, string>> StringToStringData
{
get
{
var dictionaryWithOne = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "one", "one" },
};
var dictionaryWithThree = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "one", "one" },
{ "two", "two" },
{ "three", "three" },
};
return new TheoryData<string, string, IDictionary<string, string>>
{
{ string.Empty, "[{0}]", dictionaryWithOne },
{ string.Empty, "[{0}]", dictionaryWithThree },
{ "prefix", "prefix[{0}]", dictionaryWithOne },
{ "prefix", "prefix[{0}]", dictionaryWithThree },
{ "prefix.property", "prefix.property[{0}]", dictionaryWithOne },
{ "prefix.property", "prefix.property[{0}]", dictionaryWithThree },
};
}
}
[Theory]
[MemberData(nameof(StringToStringData))]
public async Task BindModel_FallsBackToBindingValues(
string modelName,
string keyFormat,
IDictionary<string, string> dictionary)
{
// Arrange
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
context.ModelName = modelName;
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperty),
nameof(ModelWithDictionaryProperty.DictionaryProperty));
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet);
Assert.Equal(modelName, result.Key);
Assert.NotNull(result.ValidationNode);
var resultDictionary = Assert.IsAssignableFrom<IDictionary<string, string>>(result.Model);
Assert.Equal(dictionary, resultDictionary);
}
// Similar to one BindModel_FallsBackToBindingValues case but without an IEnumerableValueProvider.
[Fact]
public async Task BindModel_DoesNotFallBack_WithoutEnumerableValueProvider()
{
// Arrange
var dictionary = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "one", "one" },
{ "two", "two" },
{ "three", "three" },
};
var binder = new DictionaryModelBinder<string, string>();
var context = CreateContext();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateTestValueProvider("prefix[{0}]", dictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperty),
nameof(ModelWithDictionaryProperty.DictionaryProperty));
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode);
var resultDictionary = Assert.IsAssignableFrom<IDictionary<string, string>>(result.Model);
Assert.Empty(resultDictionary);
}
public static TheoryData<IDictionary<long, int>> LongToIntData
{
get
{
var dictionaryWithOne = new Dictionary<long, int>
{
{ 0L, 0 },
};
var dictionaryWithThree = new Dictionary<long, int>
{
{ -1L, -1 },
{ long.MaxValue, int.MaxValue },
{ long.MinValue, int.MinValue },
};
return new TheoryData<IDictionary<long, int>> { dictionaryWithOne, dictionaryWithThree };
}
}
[Theory]
[MemberData(nameof(LongToIntData))]
public async Task BindModel_FallsBackToBindingValues_WithValueTypes(IDictionary<long, int> dictionary)
{
// Arrange
var stringDictionary = dictionary.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.ToString());
var binder = new DictionaryModelBinder<long, int>();
var context = CreateContext();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider =
CreateEnumerableValueProvider("prefix[{0}]", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperty),
nameof(ModelWithDictionaryProperty.DictionaryProperty));
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode);
var resultDictionary = Assert.IsAssignableFrom<IDictionary<long, int>>(result.Model);
Assert.Equal(dictionary, resultDictionary);
}
[Fact]
public async Task BindModel_FallsBackToBindingValues_WithComplexValues()
{
// Arrange
var dictionary = new Dictionary<int, ModelWithProperties>
{
{ 23, new ModelWithProperties { Id = 43, Name = "Wilma" } },
{ 27, new ModelWithProperties { Id = 98, Name = "Fred" } },
};
var stringDictionary = new Dictionary<string, string>
{
{ "prefix[23].Id", "43" },
{ "prefix[23].Name", "Wilma" },
{ "prefix[27].Id", "98" },
{ "prefix[27].Name", "Fred" },
};
var binder = new DictionaryModelBinder<int, ModelWithProperties>();
var context = CreateContext();
context.ModelName = "prefix";
context.OperationBindingContext.ModelBinder = CreateCompositeBinder();
context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", stringDictionary);
context.ValueProvider = context.OperationBindingContext.ValueProvider;
var metadataProvider = context.OperationBindingContext.MetadataProvider;
context.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperty),
nameof(ModelWithDictionaryProperty.DictionaryProperty));
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.IsFatalError);
Assert.True(result.IsModelSet);
Assert.Equal("prefix", result.Key);
Assert.NotNull(result.ValidationNode);
var resultDictionary = Assert.IsAssignableFrom<IDictionary<int, ModelWithProperties>>(result.Model);
Assert.Equal(dictionary, resultDictionary);
}
[Fact]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding()
{
@ -161,6 +358,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
return modelBindingContext;
}
private static IModelBinder CreateCompositeBinder()
{
var binders = new IModelBinder[]
{
new TypeConverterModelBinder(),
new TypeMatchModelBinder(),
new MutableObjectModelBinder(),
new ComplexModelDtoModelBinder(),
};
return new CompositeModelBinder(binders);
}
private static IValueProvider CreateEnumerableValueProvider(
string keyFormat,
IDictionary<string, string> dictionary)
{
// Convert to an IDictionary<string, string[]> then wrap it up.
var backingStore = dictionary.ToDictionary(
kvp => string.Format(keyFormat, kvp.Key),
kvp => new[] { kvp.Value });
var stringCollection = new ReadableStringCollection(backingStore);
return new ReadableStringCollectionValueProvider(
BindingSource.Form,
stringCollection,
CultureInfo.InvariantCulture);
}
// Like CreateEnumerableValueProvider except returned instance does not implement IEnumerableValueProvider.
private static IValueProvider CreateTestValueProvider(string keyFormat, IDictionary<string, string> dictionary)
{
// Convert to an IDictionary<string, object> then wrap it up.
var backingStore = dictionary.ToDictionary(
kvp => string.Format(keyFormat, kvp.Key),
kvp => (object)kvp.Value);
return new TestValueProvider(BindingSource.Form, backingStore);
}
private static ModelBindingContext GetModelBindingContext(bool isReadOnly)
{
var metadataProvider = new TestModelMetadataProvider();
@ -208,6 +445,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
public Dictionary<string, string> DictionaryProperty { get; set; }
}
private class ModelWithProperties
{
public int Id { get; set; }
public string Name { get; set; }
public override bool Equals(object obj)
{
var other = obj as ModelWithProperties;
return other != null &&
Id == other.Id &&
string.Equals(Name, other.Name, StringComparison.Ordinal);
}
public override int GetHashCode()
{
int nameCode = Name == null ? 0 : Name.GetHashCode();
return nameCode ^ Id.GetHashCode();
}
public override string ToString()
{
return $"{{{ Id }, '{ Name }'}}";
}
}
}
}
#endif

View File

@ -16,10 +16,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
private static readonly IReadableStringCollection _backingStore = new ReadableStringCollection(
new Dictionary<string, string[]>
{
{ "foo", new[] { "fooValue1", "fooValue2"} },
{ "bar.baz", new[] {"someOtherValue" } },
{ "some", new[] { "someValue1", "someValue2" } },
{ "null_value", null },
{ "prefix.null_value", null }
{ "prefix.name", new[] { "someOtherValue" } },
{ "prefix.null_value", null },
{ "prefix.property1.property", null },
{ "prefix.property2[index]", null },
{ "prefix[index1]", null },
{ "prefix[index1].property1", null },
{ "prefix[index1].property2", null },
{ "prefix[index2].property", null },
{ "[index]", null },
{ "[index].property", null },
{ "[index][anotherIndex]", null },
});
[Fact]
@ -30,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, null);
// Act
var result = await valueProvider.ContainsPrefixAsync("");
var result = await valueProvider.ContainsPrefixAsync(string.Empty);
// Assert
Assert.False(result);
@ -43,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.ContainsPrefixAsync("");
var result = await valueProvider.ContainsPrefixAsync(string.Empty);
// Assert
Assert.True(result);
@ -56,9 +65,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act & Assert
Assert.True(await valueProvider.ContainsPrefixAsync("foo"));
Assert.True(await valueProvider.ContainsPrefixAsync("bar"));
Assert.True(await valueProvider.ContainsPrefixAsync("bar.baz"));
Assert.True(await valueProvider.ContainsPrefixAsync("some"));
Assert.True(await valueProvider.ContainsPrefixAsync("prefix"));
Assert.True(await valueProvider.ContainsPrefixAsync("prefix.name"));
Assert.True(await valueProvider.ContainsPrefixAsync("[index]"));
Assert.True(await valueProvider.ContainsPrefixAsync("prefix[index1]"));
}
[Fact]
@ -80,15 +91,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
// Arrange
var expected = new Dictionary<string, string>
{
{ "bar", "bar" },
{ "foo", "foo" },
{ "index", "[index]" },
{ "null_value", "null_value" },
{ "prefix", "prefix" }
{ "prefix", "prefix" },
{ "some", "some" },
};
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture: null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("");
var result = await valueProvider.GetKeysFromPrefixAsync(string.Empty);
// Assert
Assert.Equal(expected, result.OrderBy(kvp => kvp.Key));
@ -111,15 +122,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task GetKeysFromPrefixAsync_KnownPrefix_ReturnsMatchingItems()
{
// Arrange
var expected = new Dictionary<string, string>
{
{ "name", "prefix.name" },
{ "null_value", "prefix.null_value" },
{ "property1", "prefix.property1" },
{ "property2", "prefix.property2" },
{ "index1", "prefix[index1]" },
{ "index2", "prefix[index2]" },
};
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("bar");
var result = await valueProvider.GetKeysFromPrefixAsync("prefix");
// Assert
var kvp = Assert.Single(result);
Assert.Equal("baz", kvp.Key);
Assert.Equal("bar.baz", kvp.Value);
Assert.Equal(expected, result);
}
[Fact]
public async Task GetKeysFromPrefixAsync_IndexPrefix_ReturnsMatchingItems()
{
// Arrange
var expected = new Dictionary<string, string>
{
{ "property", "[index].property" },
{ "anotherIndex", "[index][anotherIndex]" }
};
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("[index]");
// Assert
Assert.Equal(expected, result);
}
[Fact]
@ -130,13 +166,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("bar.baz");
var result = await valueProvider.GetValueAsync("prefix.name");
// Assert
Assert.NotNull(vpResult);
Assert.Equal("someOtherValue", vpResult.RawValue);
Assert.Equal("someOtherValue", vpResult.AttemptedValue);
Assert.Equal(culture, vpResult.Culture);
Assert.NotNull(result);
Assert.Equal("someOtherValue", result.RawValue);
Assert.Equal("someOtherValue", result.AttemptedValue);
Assert.Equal(culture, result.Culture);
}
[Fact]
@ -147,13 +183,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("foo");
var result = await valueProvider.GetValueAsync("some");
// Assert
Assert.NotNull(vpResult);
Assert.Equal(new[] { "fooValue1", "fooValue2" }, (IList<string>)vpResult.RawValue);
Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue);
Assert.Equal(culture, vpResult.Culture);
Assert.NotNull(result);
Assert.Equal(new[] { "someValue1", "someValue2" }, (IList<string>)result.RawValue);
Assert.Equal("someValue1,someValue2", result.AttemptedValue);
Assert.Equal(culture, result.Culture);
}
[Theory]
@ -185,11 +221,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("key");
var result = await valueProvider.GetValueAsync("key");
// Assert
Assert.Equal(new[] { null, null, "value" }, vpResult.RawValue as IEnumerable<string>);
Assert.Equal(",,value", vpResult.AttemptedValue);
Assert.Equal(new[] { null, null, "value" }, result.RawValue as IEnumerable<string>);
Assert.Equal(",,value", result.AttemptedValue);
}
[Fact]
@ -199,10 +235,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var vpResult = await valueProvider.GetValueAsync("bar");
var result = await valueProvider.GetValueAsync("prefix");
// Assert
Assert.Null(vpResult);
Assert.Null(result);
}
[Fact]

View File

@ -101,13 +101,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
var expected = "6 feet";
// Act
var expression = await client.GetStringAsync("http://localhost/RoundTrip/GetPersonParentHeightAttribute");
var keyValuePairs = new[]
{
new KeyValuePair<string, string>(expression, expected),
};
var result = await GetPerson(client, keyValuePairs);
// Assert
Assert.Equal("Parent.Attributes[height]", expression);
// TODO: https://github.com/aspnet/Mvc/issues/1418 Requires resolution in model binding
Assert.Equal(expected, result.Parent.Attributes["height"]);
}
// Uses the expression p => p.Dependents[0].Dependents[0].Name

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -14,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
public class DictionaryModelBinderIntegrationTest
{
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefix_Success()
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndKVP_Success()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -55,7 +54,48 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
}
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success()
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndItem_Success()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Dictionary<string, int>)
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter[key0]=10");
});
var modelState = new ModelStateDictionary();
// Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext);
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
Assert.Equal(new Dictionary<string, int>() { { "key0", 10 } }, model);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("parameter[key0]", kvp.Key);
var entry = kvp.Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Theory]
[InlineData("?prefix[key0]=10")]
[InlineData("?prefix[0].Key=key0&prefix[0].Value=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success(
string queryString)
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -71,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?prefix[0].Key=key0&prefix[0].Value=10");
request.QueryString = new QueryString(queryString);
});
var modelState = new ModelStateDictionary();
@ -86,21 +126,15 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
Assert.Equal(new Dictionary<string, int>() { { "key0", 10 }, }, model);
Assert.Equal(2, modelState.Count);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Key").Value;
Assert.Equal("key0", entry.Value.AttemptedValue);
Assert.Equal("key0", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Value").Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success()
[Theory]
[InlineData("?[key0]=10")]
[InlineData("?[0].Key=key0&[0].Value=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success(string queryString)
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -112,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?[0].Key=key0&[0].Value=10");
request.QueryString = new QueryString(queryString);
});
var modelState = new ModelStateDictionary();
@ -127,17 +161,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
Assert.Equal(new Dictionary<string, int>() { { "key0", 10 }, }, model);
Assert.Equal(2, modelState.Count);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Key").Value;
Assert.Equal("key0", entry.Value.AttemptedValue);
Assert.Equal("key0", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Value").Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Fact]
@ -164,9 +190,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Empty(Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model));
Assert.Equal(0, modelState.Count);
var model = Assert.IsType<Dictionary<string, int>>(modelBindingResult.Model);
Assert.Empty(model);
Assert.Empty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
@ -174,10 +202,29 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
private class Person
{
public int Id { get; set; }
public override bool Equals(object obj)
{
var other = obj as Person;
return other != null && Id == other.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public override string ToString()
{
return $"{{ { Id } }}";
}
}
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithPrefix_Success()
[Theory]
[InlineData("?parameter[key0].Id=10")]
[InlineData("?parameter[0].Key=key0&parameter[0].Value.Id=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithPrefix_Success(string queryString)
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -189,7 +236,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?parameter[0].Key=key0&parameter[0].Value.Id=10");
request.QueryString = new QueryString(queryString);
});
var modelState = new ModelStateDictionary();
@ -202,25 +249,18 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Equal(1, model.Count);
Assert.Equal("key0", model.Keys.First());
Assert.Equal(model.Values, model.Values);
Assert.Equal(new Dictionary<string, Person> { { "key0", new Person { Id = 10 } }, }, model);
Assert.Equal(2, modelState.Count);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Key").Value;
Assert.Equal("key0", entry.Value.AttemptedValue);
Assert.Equal("key0", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Value.Id").Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefix_Success()
[Theory]
[InlineData("?prefix[key0].Id=10")]
[InlineData("?prefix[0].Key=key0&prefix[0].Value.Id=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefix_Success(
string queryString)
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -236,7 +276,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?prefix[0].Key=key0&prefix[0].Value.Id=10");
request.QueryString = new QueryString(queryString);
});
var modelState = new ModelStateDictionary();
@ -249,25 +289,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Equal(1, model.Count);
Assert.Equal("key0", model.Keys.First());
Assert.Equal(model.Values, model.Values);
Assert.Equal(new Dictionary<string, Person> { { "key0", new Person { Id = 10 } }, }, model);
Assert.Equal(2, modelState.Count);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Key").Value;
Assert.Equal("key0", entry.Value.AttemptedValue);
Assert.Equal("key0", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Value.Id").Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Fact]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_EmptyPrefix_Success()
[Theory]
[InlineData("?[key0].Id=10")]
[InlineData("?[0].Key=key0&[0].Value.Id=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_EmptyPrefix_Success(string queryString)
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
@ -279,7 +311,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request =>
{
request.QueryString = new QueryString("?[0].Key=key0&[0].Value.Id=10");
request.QueryString = new QueryString(queryString);
});
var modelState = new ModelStateDictionary();
@ -292,21 +324,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Equal(1, model.Count);
Assert.Equal("key0", model.Keys.First());
Assert.Equal(model.Values, model.Values);
Assert.Equal(new Dictionary<string, Person> { { "key0", new Person { Id = 10 } }, }, model);
Assert.Equal(2, modelState.Count);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Key").Value;
Assert.Equal("key0", entry.Value.AttemptedValue);
Assert.Equal("key0", entry.Value.RawValue);
entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Value.Id").Value;
Assert.Equal("10", entry.Value.AttemptedValue);
Assert.Equal("10", entry.Value.RawValue);
}
[Fact]
@ -333,9 +355,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
// Assert
Assert.NotNull(modelBindingResult);
Assert.True(modelBindingResult.IsModelSet);
Assert.Empty(Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model));
Assert.Equal(0, modelState.Count);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Empty(model);
Assert.Empty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}