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>
|
||||
/// This implementation handles cases like:
|
||||
/// <example>
|
||||
/// 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]'
|
||||
/// </example>
|
||||
///
|
||||
///
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
|
|
@ -35,7 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="ShortFormDictionaryValidationStrategy{TKey, TValue}"/>.
|
||||
/// </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">
|
||||
/// The <see cref="ModelMetadata"/> associated with <typeparamref name="TValue"/>.
|
||||
/// </param>
|
||||
|
|
@ -48,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mapping from model prefix key to dictionary key.
|
||||
/// Gets the mapping from <see cref="ModelStateDictionary"/> key to dictionary key.
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, TKey>> KeyMappings { get; }
|
||||
|
||||
|
|
@ -58,12 +60,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
string key,
|
||||
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 readonly string _key;
|
||||
private readonly ModelMetadata _metadata;
|
||||
private readonly IDictionary<TKey, TValue> _model;
|
||||
private readonly IEnumerator<KeyValuePair<string, TKey>> _keyMappingEnumerator;
|
||||
|
|
@ -72,14 +74,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
public Enumerator(
|
||||
ModelMetadata metadata,
|
||||
string key,
|
||||
IEnumerable<KeyValuePair<string, TKey>> keyMappings,
|
||||
IDictionary<TKey, TValue> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TValue>(valueResult.Model);
|
||||
keyMappings.Add(kvp.Key, convertedKey);
|
||||
keyMappings.Add(bindingContext.ModelName, convertedKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<int, string>(new Dictionary<string, int>()
|
||||
{
|
||||
{ "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<int, string>(new Dictionary<string, int>()
|
||||
{
|
||||
{ "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<int, string>(new Dictionary<string, int>()
|
||||
{
|
||||
{ "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(
|
||||
|
|
|
|||
|
|
@ -302,8 +302,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
Assert.Equal(
|
||||
new KeyValuePair<string, int>[]
|
||||
{
|
||||
new KeyValuePair<string, int>("23", 23),
|
||||
new KeyValuePair<string, int>("27", 27),
|
||||
new KeyValuePair<string, int>("prefix[23]", 23),
|
||||
new KeyValuePair<string, int>("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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
{
|
||||
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" } },
|
||||
{ "[]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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
[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<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)
|
||||
{
|
||||
// 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<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]
|
||||
[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<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
|
||||
public static TheoryData<Type, string, Type> DictionaryTypeData
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue