Fix binding & validating dictionaries of non-simple types in jQuery requests

- #7423
- retry failed inner bindings with alternate syntax in `ModelStateDictionary`
  - use property syntax if first attempt tried index syntax and visa versa
- instantiate `ShortFormDictionaryValidationStrategy` with full `ModelState` keys
  - can now provide exact `ModelState` keys that `ModelStateDictionary` used in inner bindings
- normalize model names without a leading period in `JQueryKeyValuePairNormalizer`

nits:
- take a few VS suggestions
This commit is contained in:
Doug Bunting 2018-04-12 21:31:32 -07:00
parent e52933e4e3
commit cc5ae02b7d
No known key found for this signature in database
GPG Key ID: 888B4EB7822B32E9
8 changed files with 484 additions and 57 deletions

View File

@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
/// <remarks> /// <remarks>
/// This implementation handles cases like: /// This implementation handles cases like:
/// <example> /// <example>
/// Model: IDictionary&lt;string, Student&gt; /// Model: IDictionary&lt;string, Student&gt;
/// Query String: ?students[Joey].Age=8&amp;students[Katherine].Age=9 /// Query String: ?students[Joey].Age=8&amp;students[Katherine].Age=9
/// ///
/// In this case, 'Joey' and 'Katherine' are the keys of the dictionary, used to bind two 'Student' /// 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 /// objects. The enumerator returned from this class will yield two 'Student' objects with corresponding
/// keys 'students[Joey]' and 'students[Katherine]' /// keys 'students[Joey]' and 'students[Katherine]'
/// </example> /// </example>
/// ///
/// Using this key format, the enumerator enumerates model objects of type <typeparamref name="TValue"/>. The /// Using this key format, the enumerator enumerates model objects of type <typeparamref name="TValue"/>. The
/// keys of the dictionary are not validated as they must be simple types. /// keys of the dictionary are not validated as they must be simple types.
/// </remarks> /// </remarks>
@ -35,7 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
/// <summary> /// <summary>
/// Creates a new <see cref="ShortFormDictionaryValidationStrategy{TKey, TValue}"/>. /// Creates a new <see cref="ShortFormDictionaryValidationStrategy{TKey, TValue}"/>.
/// </summary> /// </summary>
/// <param name="keyMappings">The mapping from model prefix key to dictionary key.</param> /// <param name="keyMappings">
/// The mapping from <see cref="ModelStateDictionary"/> key to dictionary key.
/// </param>
/// <param name="valueMetadata"> /// <param name="valueMetadata">
/// The <see cref="ModelMetadata"/> associated with <typeparamref name="TValue"/>. /// The <see cref="ModelMetadata"/> associated with <typeparamref name="TValue"/>.
/// </param> /// </param>
@ -48,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
/// <summary> /// <summary>
/// Gets the mapping from model prefix key to dictionary key. /// Gets the mapping from <see cref="ModelStateDictionary"/> key to dictionary key.
/// </summary> /// </summary>
public IEnumerable<KeyValuePair<string, TKey>> KeyMappings { get; } public IEnumerable<KeyValuePair<string, TKey>> KeyMappings { get; }
@ -58,12 +60,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string key, string key,
object model) object model)
{ {
return new Enumerator(_valueMetadata, key, KeyMappings, (IDictionary<TKey, TValue>)model); // key is not needed because KeyMappings maps from full ModelState keys to dictionary keys.
return new Enumerator(_valueMetadata, KeyMappings, (IDictionary<TKey, TValue>)model);
} }
private class Enumerator : IEnumerator<ValidationEntry> private class Enumerator : IEnumerator<ValidationEntry>
{ {
private readonly string _key;
private readonly ModelMetadata _metadata; private readonly ModelMetadata _metadata;
private readonly IDictionary<TKey, TValue> _model; private readonly IDictionary<TKey, TValue> _model;
private readonly IEnumerator<KeyValuePair<string, TKey>> _keyMappingEnumerator; private readonly IEnumerator<KeyValuePair<string, TKey>> _keyMappingEnumerator;
@ -72,14 +74,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public Enumerator( public Enumerator(
ModelMetadata metadata, ModelMetadata metadata,
string key,
IEnumerable<KeyValuePair<string, TKey>> keyMappings, IEnumerable<KeyValuePair<string, TKey>> keyMappings,
IDictionary<TKey, TValue> model) IDictionary<TKey, TValue> model)
{ {
_metadata = metadata; _metadata = metadata;
_key = key;
_model = model; _model = model;
_keyMappingEnumerator = keyMappings.GetEnumerator(); _keyMappingEnumerator = keyMappings.GetEnumerator();
} }
@ -104,10 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
var key = ModelNames.CreateIndexModelName(_key, _keyMappingEnumerator.Current.Key); _entry = new ValidationEntry(_metadata, _keyMappingEnumerator.Current.Key, value);
var model = value;
_entry = new ValidationEntry(_metadata, key, model);
return true; return true;
} }

View File

@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Logger.NoKeyValueFormatForDictionaryModelBinder(bindingContext); Logger.NoKeyValueFormatForDictionaryModelBinder(bindingContext);
var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider; if (!(bindingContext.ValueProvider is IEnumerableValueProvider enumerableValueProvider))
if (enumerableValueProvider == null)
{ {
// No IEnumerableValueProvider available for the fallback approach. For example the user may have // No IEnumerableValueProvider available for the fallback approach. For example the user may have
// replaced the ValueProvider with something other than a CompositeValueProvider. // 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. // 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) if (keys.Count == 0)
{ {
// No entries with the expected keys. // No entries with the expected keys.
@ -117,10 +117,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
await _valueBinder.BindModelAsync(bindingContext); await _valueBinder.BindModelAsync(bindingContext);
var valueResult = bindingContext.Result; 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. // Always add an entry to the dictionary but validate only if binding was successful.
model[convertedKey] = ModelBindingHelper.CastOrDefault<TValue>(valueResult.Model); model[convertedKey] = ModelBindingHelper.CastOrDefault<TValue>(valueResult.Model);
keyMappings.Add(kvp.Key, convertedKey); keyMappings.Add(bindingContext.ModelName, convertedKey);
} }
} }

View File

@ -86,7 +86,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
else else
{ {
// Field name. Convert to dot notation. // 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); builder.Append(key, indexOpen + 1, indexClose - indexOpen - 1);
} }

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
{ {
public class ShortFormDictionaryValidationStrategyTest public class ShortFormDictionaryValidationStrategyTest
@ -28,14 +27,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>() var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>()
{ {
{ "2", 2 }, { "prefix[2]", 2 },
{ "3", 3 }, { "prefix[3]", 3 },
{ "5", 5 }, { "prefix[5]", 5 },
}, },
valueMetadata); valueMetadata);
// Act // Act
var enumerator = strategy.GetChildren(metadata, "prefix", model); var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -76,13 +75,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>() var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>()
{ {
{ "2", 2 }, { "prefix[2]", 2 },
{ "3", 3 }, { "prefix[3]", 3 },
}, },
valueMetadata); valueMetadata);
// Act // Act
var enumerator = strategy.GetChildren(metadata, "prefix", model); var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert // Assert
Assert.Collection( Assert.Collection(
@ -116,14 +115,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var valueMetadata = metadataProvider.GetMetadataForType(typeof(string)); var valueMetadata = metadataProvider.GetMetadataForType(typeof(string));
var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>() var strategy = new ShortFormDictionaryValidationStrategy<int, string>(new Dictionary<string, int>()
{ {
{ "2", 2 }, { "prefix[2]", 2 },
{ "3", 3 }, { "prefix[3]", 3 },
{ "5", 5 }, { "prefix[5]", 5 },
}, },
valueMetadata); valueMetadata);
// Act // Act
var enumerator = strategy.GetChildren(metadata, "prefix", model); var enumerator = strategy.GetChildren(metadata, "ignored prefix", model);
// Assert // Assert
Assert.Collection( Assert.Collection(

View File

@ -302,8 +302,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal( Assert.Equal(
new KeyValuePair<string, int>[] new KeyValuePair<string, int>[]
{ {
new KeyValuePair<string, int>("23", 23), new KeyValuePair<string, int>("prefix[23]", 23),
new KeyValuePair<string, int>("27", 27), new KeyValuePair<string, int>("prefix[27]", 27),
}.OrderBy(kvp => kvp.Key), }.OrderBy(kvp => kvp.Key),
strategy.KeyMappings.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) public override bool Equals(object obj)
{ {
var other = obj as ModelWithProperties; return obj is ModelWithProperties other &&
return other != null &&
Id == other.Id && Id == other.Id &&
string.Equals(Name, other.Name, StringComparison.Ordinal); string.Equals(Name, other.Name, StringComparison.Ordinal);
} }
public override int GetHashCode() public override int GetHashCode()
{ {
int nameCode = Name == null ? 0 : Name.GetHashCode(); var nameCode = Name == null ? 0 : Name.GetHashCode();
return nameCode ^ Id.GetHashCode(); return nameCode ^ Id.GetHashCode();
} }

View File

@ -89,12 +89,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
"[14]property1[15]property2", "[14]property1[15]property2",
"prefix.property1[13]", "prefix.property1[13]",
"prefix[14][15]", "prefix[14][15]",
".property5", "property5",
".property6Value", "property6Value",
"prefix.property2", "prefix.property2",
"prefix.propertyValue", "prefix.propertyValue",
".property7.property8", "property7.property8",
".property9.property10Value", "property9.property10Value",
}; };
} }
} }

View File

@ -17,6 +17,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ {
private static readonly Dictionary<string, StringValues> _backingStore = new Dictionary<string, StringValues> private static readonly Dictionary<string, StringValues> _backingStore = new Dictionary<string, StringValues>
{ {
// 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 <input/> 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" } }, { "[]", new[] { "found" } },
{ "[]property1", new[] { "found" } }, { "[]property1", new[] { "found" } },
{ "property2[]", new[] { "found" } }, { "property2[]", new[] { "found" } },
@ -57,12 +63,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
"[14]property1[15]property2", "[14]property1[15]property2",
"prefix.property1[13]", "prefix.property1[13]",
"prefix[14][15]", "prefix[14][15]",
".property5", "property5",
".property6Value", "property6Value",
"prefix.property2", "prefix.property2",
"prefix.propertyValue", "prefix.propertyValue",
".property7.property8", "property7.property8",
".property9.property10Value", "property9.property10Value",
}; };
} }
} }

View File

@ -450,15 +450,26 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
} }
} }
public static TheoryData<string> ComplexType_ImpliedPrefixData
{
get
{
return new TheoryData<string>
{
"?[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&parameter[0].Value.Id=10",
"?parameter.index=low&parameter[low].Key=key0&parameter[low].Value.Id=10",
"?parameter.index=index&parameter[index].Key=key0&parameter[index].Value.Id=10",
};
}
}
[Theory] [Theory]
[InlineData("?[key0].Id=10")] [MemberData(nameof(ComplexType_ImpliedPrefixData))]
[InlineData("?[0].Key=key0&[0].Value.Id=10")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefix(string queryString)
[InlineData("?index=low&[low].Key=key0&[low].Value.Id=10")]
[InlineData("?parameter[key0].Id=10")]
[InlineData("?parameter[0].Key=key0&parameter[0].Value.Id=10")]
[InlineData("?parameter.index=low&parameter[low].Key=key0&parameter[low].Value.Id=10")]
[InlineData("?parameter.index=index&parameter[index].Key=key0&parameter[index].Value.Id=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_Success(string queryString)
{ {
// Arrange // Arrange
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
@ -490,11 +501,168 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
} }
[Theory] [Theory]
[InlineData("?prefix[key0].Id=10")] [InlineData("?[key0][Id]=10")] // Both key segments will be rewritten.
[InlineData("?prefix[0].Key=key0&prefix[0].Value.Id=10")] [InlineData("?[0][Key]=key0&[0][Value][Id]=10")]
[InlineData("?prefix.index=low&prefix[low].Key=key0&prefix[low].Value.Id=10")] [InlineData("?parameter[key0][Id]=10")]
[InlineData("?prefix.index=index&prefix[index].Key=key0&prefix[index].Value.Id=10")] [InlineData("?parameter[0][Key]=key0&parameter[0][Value][Id]=10")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ExplicitPrefix_Success( [MemberData(nameof(ComplexType_ImpliedPrefixData))]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixAndJQuery(
string queryString)
{
// Arrange
var expectedDictionary = new Dictionary<string, Person> { { "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<string, Person>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(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&parameter[0][Value][Id]=10")]
[MemberData(nameof(ComplexType_ImpliedPrefixData))]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithImpliedPrefixAndJQueryFirst(
string queryString)
{
// Arrange
var expectedDictionary = new Dictionary<string, Person> { { "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<string, Person>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(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<string, Person> { { "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<string, Person>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(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<string, Person> { { "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<string, Person>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Equal(expectedDictionary, model);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
public static TheoryData<string> ComplexType_ExplicitPrefixData
{
get
{
return new TheoryData<string>
{
"?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) string queryString)
{ {
// Arrange // Arrange
@ -530,6 +698,45 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid); 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<string, Person>)
};
var modelState = testContext.ModelState;
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, Person>>(modelBindingResult.Model);
Assert.Equal(new Dictionary<string, Person> { { "key0", new Person { Id = 10 } }, }, model);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
[Theory] [Theory]
[InlineData("?[key0].Id=100")] [InlineData("?[key0].Id=100")]
[InlineData("?[0].Key=key0&[0].Value.Id=100")] [InlineData("?[0].Key=key0&[0].Value.Id=100")]
@ -610,6 +817,202 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
} }
public static TheoryData<string> CollectionType_ImpliedPrefixData
{
get
{
return new TheoryData<string>
{
"?[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&parameter[key0]=11",
"?parameter[key0][0]=10&parameter[key0][1]=11",
"?parameter[0].Key=key0&parameter[0].Value[0]=10&parameter[0].Value[1]=11",
"?parameter.index=low&parameter[low].Key=key0&parameter[low].Value[0]=10&parameter[low].Value[1]=11",
"?parameter.index=index&parameter[index].Key=key0&parameter[index].Value[0]=10&parameter[index].Value[1]=11",
};
}
}
[Theory]
[MemberData(nameof(CollectionType_ImpliedPrefixData))]
public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefix(string queryString)
{
// Arrange
var expectedDictionary = new Dictionary<string, string[]> { { "key0", new[] { "10", "11" } } };
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Dictionary<string, string[]>)
};
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<Dictionary<string, string[]>>(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<string, string[]> { { "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<string, string[]>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, string[]>>(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<string, string[]> { { "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<string, string[]>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, string[]>>(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&parameter[42]=11")]
[InlineData("?parameter[42][]=10&parameter[42][]=11")]
[InlineData("?parameter[42][0]=10&parameter[42][1]=11")]
public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixIntegralKeysAndJQuery(
string queryString)
{
// Arrange
var expectedDictionary = new Dictionary<string, string[]> { { "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<string, string[]>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, string[]>>(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&parameter[42]=11")]
[InlineData("?parameter[42][]=10&parameter[42][]=11")]
[InlineData("?parameter[42][0]=10&parameter[42][1]=11")]
public async Task DictionaryModelBinder_BindsDictionaryOfCollectionType_WithImpliedPrefixIntegralKeysAndJQueryFirst(
string queryString)
{
// Arrange
var expectedDictionary = new Dictionary<string, string[]> { { "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<string, string[]>)
};
// Act
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelBindingResult.IsModelSet);
var model = Assert.IsType<Dictionary<string, string[]>>(modelBindingResult.Model);
Assert.Equal(expectedDictionary, model);
Assert.NotEmpty(modelState);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid);
}
// parameter type, query string, expected type // parameter type, query string, expected type
public static TheoryData<Type, string, Type> DictionaryTypeData public static TheoryData<Type, string, Type> DictionaryTypeData
{ {