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:
parent
e52933e4e3
commit
cc5ae02b7d
|
|
@ -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<string, Student>
|
/// Model: IDictionary<string, Student>
|
||||||
/// Query String: ?students[Joey].Age=8&students[Katherine].Age=9
|
/// 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'
|
/// 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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¶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]
|
[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¶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)
|
|
||||||
{
|
{
|
||||||
// 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¶meter[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¶meter[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¶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<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¶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<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¶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<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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue