diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ShortFormDictionaryValidationStrategy.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ShortFormDictionaryValidationStrategy.cs
index 0da31c0cbe..31d1db97bf 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ShortFormDictionaryValidationStrategy.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ShortFormDictionaryValidationStrategy.cs
@@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
///
/// This implementation handles cases like:
///
- /// Model: IDictionary<string, Student>
+ /// Model: IDictionary<string, Student>
/// Query String: ?students[Joey].Age=8&students[Katherine].Age=9
- ///
+ ///
/// In this case, 'Joey' and 'Katherine' are the keys of the dictionary, used to bind two 'Student'
/// objects. The enumerator returned from this class will yield two 'Student' objects with corresponding
/// keys 'students[Joey]' and 'students[Katherine]'
///
- ///
+ ///
/// Using this key format, the enumerator enumerates model objects of type . The
/// keys of the dictionary are not validated as they must be simple types.
///
@@ -35,7 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
///
/// Creates a new .
///
- /// The mapping from model prefix key to dictionary key.
+ ///
+ /// The mapping from key to dictionary key.
+ ///
///
/// The associated with .
///
@@ -48,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
///
- /// Gets the mapping from model prefix key to dictionary key.
+ /// Gets the mapping from key to dictionary key.
///
public IEnumerable> KeyMappings { get; }
@@ -58,12 +60,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string key,
object model)
{
- return new Enumerator(_valueMetadata, key, KeyMappings, (IDictionary)model);
+ // key is not needed because KeyMappings maps from full ModelState keys to dictionary keys.
+ return new Enumerator(_valueMetadata, KeyMappings, (IDictionary)model);
}
private class Enumerator : IEnumerator
{
- private readonly string _key;
private readonly ModelMetadata _metadata;
private readonly IDictionary _model;
private readonly IEnumerator> _keyMappingEnumerator;
@@ -72,14 +74,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public Enumerator(
ModelMetadata metadata,
- string key,
IEnumerable> keyMappings,
IDictionary model)
{
_metadata = metadata;
- _key = key;
_model = model;
-
_keyMappingEnumerator = keyMappings.GetEnumerator();
}
@@ -104,10 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
- var key = ModelNames.CreateIndexModelName(_key, _keyMappingEnumerator.Current.Key);
- var model = value;
-
- _entry = new ValidationEntry(_metadata, key, model);
+ _entry = new ValidationEntry(_metadata, _keyMappingEnumerator.Current.Key, value);
return true;
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs
index dc967fefdf..fbb0c53db1 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DictionaryModelBinder.cs
@@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Logger.NoKeyValueFormatForDictionaryModelBinder(bindingContext);
- var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
- if (enumerableValueProvider == null)
+ if (!(bindingContext.ValueProvider is IEnumerableValueProvider enumerableValueProvider))
{
// No IEnumerableValueProvider available for the fallback approach. For example the user may have
// replaced the ValueProvider with something other than a CompositeValueProvider.
@@ -90,7 +89,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
}
// Attempt to bind dictionary from a set of prefix[key]=value entries. Get the short and long keys first.
- var keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
+ var prefix = bindingContext.ModelName;
+ var keys = enumerableValueProvider.GetKeysFromPrefix(prefix);
if (keys.Count == 0)
{
// No entries with the expected keys.
@@ -117,10 +117,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
await _valueBinder.BindModelAsync(bindingContext);
var valueResult = bindingContext.Result;
+ if (!valueResult.IsModelSet)
+ {
+ // Factories for IKeyRewriterValueProvider implementations are not all-or-nothing i.e.
+ // "[key][propertyName]" may be rewritten as ".key.propertyName" or "[key].propertyName". Try
+ // again in case this scope is binding a complex type and rewriting
+ // landed on ".key.propertyName" or in case this scope is binding another collection and an
+ // IKeyRewriterValueProvider implementation was first (hiding the original "[key][next key]").
+ if (kvp.Value.EndsWith("]"))
+ {
+ bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, kvp.Key);
+ }
+ else
+ {
+ bindingContext.ModelName = ModelNames.CreateIndexModelName(prefix, kvp.Key);
+ }
+
+ await _valueBinder.BindModelAsync(bindingContext);
+ valueResult = bindingContext.Result;
+ }
// Always add an entry to the dictionary but validate only if binding was successful.
model[convertedKey] = ModelBindingHelper.CastOrDefault(valueResult.Model);
- keyMappings.Add(kvp.Key, convertedKey);
+ keyMappings.Add(bindingContext.ModelName, convertedKey);
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs
index b9fcc2520f..b0c1c28c0f 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs
@@ -86,7 +86,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
else
{
// Field name. Convert to dot notation.
- builder.Append('.');
+ if (builder.Length != 0)
+ {
+ // Was x[field], not [field] or [][field].
+ builder.Append('.');
+ }
+
builder.Append(key, indexOpen + 1, indexClose - indexOpen - 1);
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ShortFormDictionaryValidationStrategyTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ShortFormDictionaryValidationStrategyTest.cs
index 582b2800ea..9ef857f0a1 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ShortFormDictionaryValidationStrategyTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ShortFormDictionaryValidationStrategyTest.cs
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
-
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ShortFormDictionaryValidationStrategyTest
@@ -28,14 +27,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary()
{
- { "2", 2 },
- { "3", 3 },
- { "5", 5 },
+ { "prefix[2]", 2 },
+ { "prefix[3]", 3 },
+ { "prefix[5]", 5 },
},
valueMetadata);
// Act
- var enumerator = strategy.GetChildren(metadata, "prefix", model);
+ var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert
Assert.Collection(
@@ -76,13 +75,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary()
{
- { "2", 2 },
- { "3", 3 },
+ { "prefix[2]", 2 },
+ { "prefix[3]", 3 },
},
valueMetadata);
// Act
- var enumerator = strategy.GetChildren(metadata, "prefix", model);
+ var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert
Assert.Collection(
@@ -116,14 +115,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy(new Dictionary()
{
- { "2", 2 },
- { "3", 3 },
- { "5", 5 },
+ { "prefix[2]", 2 },
+ { "prefix[3]", 3 },
+ { "prefix[5]", 5 },
},
valueMetadata);
// Act
- var enumerator = strategy.GetChildren(metadata, "prefix", model);
+ var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert
Assert.Collection(
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs
index c155ed8b03..2beb2a33f9 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs
@@ -302,8 +302,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(
new KeyValuePair[]
{
- new KeyValuePair("23", 23),
- new KeyValuePair("27", 27),
+ new KeyValuePair("prefix[23]", 23),
+ new KeyValuePair("prefix[27]", 27),
}.OrderBy(kvp => kvp.Key),
strategy.KeyMappings.OrderBy(kvp => kvp.Key));
}
@@ -532,15 +532,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public override bool Equals(object obj)
{
- var other = obj as ModelWithProperties;
- return other != null &&
+ return obj is ModelWithProperties other &&
Id == other.Id &&
string.Equals(Name, other.Name, StringComparison.Ordinal);
}
public override int GetHashCode()
{
- int nameCode = Name == null ? 0 : Name.GetHashCode();
+ var nameCode = Name == null ? 0 : Name.GetHashCode();
return nameCode ^ Id.GetHashCode();
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
index b968a92f19..d940134a8d 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
@@ -89,12 +89,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
"[14]property1[15]property2",
"prefix.property1[13]",
"prefix[14][15]",
- ".property5",
- ".property6Value",
+ "property5",
+ "property6Value",
"prefix.property2",
"prefix.propertyValue",
- ".property7.property8",
- ".property9.property10Value",
+ "property7.property8",
+ "property9.property10Value",
};
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs
index a3133261c3..70c3edc6a2 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs
@@ -17,6 +17,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{
private static readonly Dictionary _backingStore = new Dictionary
{
+ // Reviewers: A fair number of cases in the following dataset seem impossible for jQuery.ajax() to send
+ // given how $.ajax() and $.param() are defined i.e. $.param() won't add a bare name after ']'. Also, the
+ // names we generate for elements are always valid. Should we remove these weird / invalid cases
+ // e.g. normalizing "property[]Value"? (That normalizes to "propertyValue".) See also
+ // https://github.com/jquery/jquery/blob/1ea092a54b00aa4d902f4e22ada3854d195d4a18/src/serialize.js#L13-L92
+ // The alternative is to handle "[]name" and "[index]name" cases while normalizing keys.
{ "[]", new[] { "found" } },
{ "[]property1", new[] { "found" } },
{ "property2[]", new[] { "found" } },
@@ -57,12 +63,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
"[14]property1[15]property2",
"prefix.property1[13]",
"prefix[14][15]",
- ".property5",
- ".property6Value",
+ "property5",
+ "property6Value",
"prefix.property2",
"prefix.propertyValue",
- ".property7.property8",
- ".property9.property10Value",
+ "property7.property8",
+ "property9.property10Value",
};
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
index 79a77e57f3..c19e7cfac5 100644
--- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
@@ -450,15 +450,26 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
}
+ public static TheoryData ComplexType_ImpliedPrefixData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ "?[key0].Id=10",
+ "?[0].Key=key0&[0].Value.Id=10",
+ "?index=low&[low].Key=key0&[low].Value.Id=10",
+ "?parameter[key0].Id=10",
+ "?parameter[0].Key=key0¶meter[0].Value.Id=10",
+ "?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=10",
+ "?parameter.index=index¶meter[index].Key=key0¶meter[index].Value.Id=10",
+ };
+ }
+ }
+
[Theory]
- [InlineData("?[key0].Id=10")]
- [InlineData("?[0].Key=key0&[0].Value.Id=10")]
- [InlineData("?index=low&[low].Key=key0&[low].Value.Id=10")]
- [InlineData("?parameter[key0].Id=10")]
- [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=10")]
- [InlineData("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=10")]
- [InlineData("?parameter.index=index¶meter[index].Key=key0¶meter[index].Value.Id=10")]
- public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_Success(string queryString)
+ [MemberData(nameof(ComplexType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefix(string queryString)
{
// Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
@@ -490,11 +501,168 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
[Theory]
- [InlineData("?prefix[key0].Id=10")]
- [InlineData("?prefix[0].Key=key0&prefix[0].Value.Id=10")]
- [InlineData("?prefix.index=low&prefix[low].Key=key0&prefix[low].Value.Id=10")]
- [InlineData("?prefix.index=index&prefix[index].Key=key0&prefix[index].Value.Id=10")]
- public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ExplicitPrefix_Success(
+ [InlineData("?[key0][Id]=10")] // Both key segments will be rewritten.
+ [InlineData("?[0][Key]=key0&[0][Value][Id]=10")]
+ [InlineData("?parameter[key0][Id]=10")]
+ [InlineData("?parameter[0][Key]=key0¶meter[0][Value][Id]=10")]
+ [MemberData(nameof(ComplexType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixAndJQuery(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "key0", new Person { Id = 10 } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory after default factories.
+ options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [InlineData("?[key0][Id]=10")] // Both key segments will be rewritten.
+ [InlineData("?[0][Key]=key0&[0][Value][Id]=10")]
+ [InlineData("?parameter[key0][Id]=10")]
+ [InlineData("?parameter[0][Key]=key0¶meter[0][Value][Id]=10")]
+ [MemberData(nameof(ComplexType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixAndJQueryFirst(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "key0", new Person { Id = 10 } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory before default factories.
+ options => options.ValueProviderFactories.Insert(0, new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [InlineData("?[42][Id]=10")] // Only Id segment will be rewritten.
+ [InlineData("?parameter[42][Id]=10")]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixIntegralKeysAndJQuery(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "42", new Person { Id = 10 } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory after default factories.
+ options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [InlineData("?[42][Id]=10")] // Only Id segment will be rewritten.
+ [InlineData("?parameter[42][Id]=10")]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixIntegralKeysAndJQueryFirst(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "42", new Person { Id = 10 } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory before default factories.
+ options => options.ValueProviderFactories.Insert(0, new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ public static TheoryData ComplexType_ExplicitPrefixData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ "?prefix[key0].Id=10",
+ "?prefix[0].Key=key0&prefix[0].Value.Id=10",
+ "?prefix.index=low&prefix[low].Key=key0&prefix[low].Value.Id=10",
+ "?prefix.index=index&prefix[index].Key=key0&prefix[index].Value.Id=10",
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ComplexType_ExplicitPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefix(
string queryString)
{
// Arrange
@@ -530,6 +698,45 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid);
}
+ [Theory]
+ [InlineData("?prefix[key0][Id]=10")]
+ [MemberData(nameof(ComplexType_ExplicitPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefixAndJQuery(
+ string queryString)
+ {
+ // Arrange
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory after default factories.
+ options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()));
+
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ BindingInfo = new BindingInfo()
+ {
+ BinderModelName = "prefix",
+ },
+ ParameterType = typeof(Dictionary)
+ };
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(new Dictionary { { "key0", new Person { Id = 10 } }, }, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
[Theory]
[InlineData("?[key0].Id=100")]
[InlineData("?[0].Key=key0&[0].Value.Id=100")]
@@ -610,6 +817,202 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid);
}
+ public static TheoryData CollectionType_ImpliedPrefixData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ "?[key0]=10&[key0]=11",
+ "?[key0][0]=10&[key0][1]=11",
+ "?[0].Key=key0&[0].Value[0]=10&[0].Value[1]=11",
+ "?index=low&[low].Key=key0&[low].Value[0]=10&[low].Value[1]=11",
+ "?parameter[key0]=10¶meter[key0]=11",
+ "?parameter[key0][0]=10¶meter[key0][1]=11",
+ "?parameter[0].Key=key0¶meter[0].Value[0]=10¶meter[0].Value[1]=11",
+ "?parameter.index=low¶meter[low].Key=key0¶meter[low].Value[0]=10¶meter[low].Value[1]=11",
+ "?parameter.index=index¶meter[index].Key=key0¶meter[index].Value[0]=10¶meter[index].Value[1]=11",
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(CollectionType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefix(string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "key0", new[] { "10", "11" } } };
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString));
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [MemberData(nameof(CollectionType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixAndJQuery(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "key0", new[] { "10", "11" } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory after default factories.
+ options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [MemberData(nameof(CollectionType_ImpliedPrefixData))]
+ public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixAndJQueryFirst(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "key0", new[] { "10", "11" } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory before default factories.
+ options => options.ValueProviderFactories.Insert(0, new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [InlineData("?[42]=10&[42]=11")]
+ [InlineData("?[42][]=10&[42][]=11")]
+ [InlineData("?[42][0]=10&[42][1]=11")]
+ [InlineData("?parameter[42]=10¶meter[42]=11")]
+ [InlineData("?parameter[42][]=10¶meter[42][]=11")]
+ [InlineData("?parameter[42][0]=10¶meter[42][1]=11")]
+ public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixIntegralKeysAndJQuery(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "42", new[] { "10", "11" } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory after default factories.
+ options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
+ [Theory]
+ [InlineData("?[42]=10&[42]=11")]
+ [InlineData("?[42][]=10&[42][]=11")]
+ [InlineData("?[42][0]=10&[42][1]=11")]
+ [InlineData("?parameter[42]=10¶meter[42]=11")]
+ [InlineData("?parameter[42][]=10¶meter[42][]=11")]
+ [InlineData("?parameter[42][0]=10¶meter[42][1]=11")]
+ public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixIntegralKeysAndJQueryFirst(
+ string queryString)
+ {
+ // Arrange
+ var expectedDictionary = new Dictionary { { "42", new[] { "10", "11" } } };
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = new QueryString(queryString),
+ // Add JQueryQueryStringValueProviderFactory before default factories.
+ options => options.ValueProviderFactories.Insert(0, new JQueryQueryStringValueProviderFactory()));
+
+ var modelState = testContext.ModelState;
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(Dictionary)
+ };
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ Assert.True(modelBindingResult.IsModelSet);
+
+ var model = Assert.IsType>(modelBindingResult.Model);
+ Assert.Equal(expectedDictionary, model);
+
+ Assert.NotEmpty(modelState);
+ Assert.Equal(0, modelState.ErrorCount);
+ Assert.True(modelState.IsValid);
+ }
+
// parameter type, query string, expected type
public static TheoryData DictionaryTypeData
{