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>
/// This implementation handles cases like:
/// <example>
/// Model: IDictionary&lt;string, Student&gt;
/// Model: IDictionary&lt;string, Student&gt;
/// 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'
/// 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;
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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(

View File

@ -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();
}

View File

@ -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",
};
}
}

View File

@ -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",
};
}
}

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]
[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&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)
[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&parameter[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&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)
{
// 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&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
public static TheoryData<Type, string, Type> DictionaryTypeData
{