From 79a2982441d65373af93d612ee2d5f065c4db9b7 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Sun, 19 Jul 2015 22:05:58 -0700 Subject: [PATCH] Add support for model binding dictionaries from `prefix[name]=value` entries - #1418 - add new fallback binding in `DictionaryModelBinder` - similar to MVC 5 approach but more explicit and with better key conversion support - fix bugs in `PrefixContainer` encountered while adding new tests of #1418 scenarios - did not handle entries like "[key]" or "prefix.key[index]" correctly - refactor part of `GetKeyFromEmptyPrefix()` into `IndexOfDelimiter()`; share with `GetKeyFromNonEmptyPrefix()` - extend `ReadableStringCollectionValueProviderTest` to cover bracketed key segments nits: - remove use of "foo", "bar", and "baz" in affected test classes - `""` -> `string.Empty` - `vpResult` -> `result` --- .../ModelBinding/DictionaryModelBinder.cs | 76 +++++ .../ModelBinding/PrefixContainer.cs | 76 +++-- .../ModelBinding/DictionaryModelBinderTest.cs | 263 ++++++++++++++++++ ...adableStringCollectionValueProviderTest.cs | 98 ++++--- .../RoundTripTests.cs | 8 +- .../DictionaryModelBinderIntegrationTest.cs | 172 +++++++----- 6 files changed, 565 insertions(+), 128 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs index 24384b1ec2..26c3dab010 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/DictionaryModelBinder.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -13,15 +16,88 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// Type of values in the dictionary. public class DictionaryModelBinder : CollectionModelBinder> { + /// + public override async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) + { + var result = await base.BindModelAsync(bindingContext); + if (result == null || !result.IsModelSet) + { + // No match for the prefix at all. + return result; + } + + Debug.Assert(result.Model != null); + var model = (Dictionary)result.Model; + if (model.Count != 0) + { + // ICollection> approach was successful. + return result; + } + + var enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider; + if (enumerableValueProvider == null) + { + // No IEnumerableValueProvider available for the fallback approach. For example the user may have + // replaced the ValueProvider with something other than a CompositeValueProvider. + return result; + } + + // Attempt to bind dictionary from a set of prefix[key]=value entries. Get the short and long keys first. + var keys = await enumerableValueProvider.GetKeysFromPrefixAsync(bindingContext.ModelName); + if (!keys.Any()) + { + // No entries with the expected keys. + return result; + } + + // Update the existing successful but empty ModelBindingResult. + var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; + var valueMetadata = metadataProvider.GetMetadataForType(typeof(TValue)); + var valueBindingContext = ModelBindingContext.GetChildModelBindingContext( + bindingContext, + bindingContext.ModelName, + valueMetadata); + + var modelBinder = bindingContext.OperationBindingContext.ModelBinder; + var validationNode = result.ValidationNode; + + foreach (var key in keys) + { + var dictionaryKey = ConvertFromString(key.Key); + valueBindingContext.ModelName = key.Value; + + var valueResult = await modelBinder.BindModelAsync(valueBindingContext); + + // Always add an entry to the dictionary but validate only if binding was successful. + model[dictionaryKey] = ModelBindingHelper.CastOrDefault(valueResult?.Model); + if (valueResult != null && valueResult.IsModelSet) + { + validationNode.ChildNodes.Add(valueResult.ValidationNode); + } + } + + return result; + } + /// protected override object GetModel(IEnumerable> newCollection) { return newCollection?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } + /// protected override object CreateEmptyCollection() { return new Dictionary(); } + + private static TKey ConvertFromString(string keyString) + { + // Use InvariantCulture to convert string since ExpressionHelper.GetExpressionText() used that culture. + var keyResult = new ValueProviderResult(keyString); + var keyObject = keyResult.ConvertTo(typeof(TKey)); + + return ModelBindingHelper.CastOrDefault(keyObject); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/PrefixContainer.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/PrefixContainer.cs index 0d193d271f..636148cf4e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/PrefixContainer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/PrefixContainer.cs @@ -81,31 +81,34 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private static void GetKeyFromEmptyPrefix(string entry, IDictionary results) { - var dotPosition = entry.IndexOf('.'); - var bracketPosition = entry.IndexOf('['); - var delimiterPosition = -1; + string key; + string fullName; + var delimiterPosition = IndexOfDelimiter(entry, 0); - if (dotPosition == -1) + if (delimiterPosition == 0 && entry[0] == '[') { - if (bracketPosition != -1) + // Handle an entry such as "[key]". + var bracketPosition = entry.IndexOf(']', 1); + if (bracketPosition == -1) { - delimiterPosition = bracketPosition; + // Malformed for dictionary. + return; } + + key = entry.Substring(1, bracketPosition - 1); + fullName = entry.Substring(0, bracketPosition + 1); } else { - if (bracketPosition == -1) - { - delimiterPosition = dotPosition; - } - else - { - delimiterPosition = Math.Min(dotPosition, bracketPosition); - } + // Handle an entry such as "key", "key.property" and "key[index]". + key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition); + fullName = key; } - var key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition); - results[key] = key; + if (!results.ContainsKey(key)) + { + results.Add(key, fullName); + } } private static void GetKeyFromNonEmptyPrefix(string prefix, string entry, IDictionary results) @@ -117,17 +120,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding switch (entry[prefix.Length]) { case '.': - var dotPosition = entry.IndexOf('.', keyPosition); - if (dotPosition == -1) + // Handle an entry such as "prefix.key", "prefix.key.property" and "prefix.key[index]". + var delimiterPosition = IndexOfDelimiter(entry, keyPosition); + if (delimiterPosition == -1) { - dotPosition = entry.Length; + // Neither '.' nor '[' found later in the name. Use rest of the string. + key = entry.Substring(keyPosition); + fullName = entry; + } + else + { + key = entry.Substring(keyPosition, delimiterPosition - keyPosition); + fullName = entry.Substring(0, delimiterPosition); } - - key = entry.Substring(keyPosition, dotPosition - keyPosition); - fullName = entry.Substring(0, dotPosition); break; case '[': + // Handle an entry such as "prefix[key]". var bracketPosition = entry.IndexOf(']', keyPosition); if (bracketPosition == -1) { @@ -140,6 +149,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding break; default: + // Ignore an entry such as "prefixA". return; } @@ -188,6 +198,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + private static int IndexOfDelimiter(string entry, int startIndex) + { + int delimiterPosition; + var bracketPosition = entry.IndexOf('[', startIndex); + var dotPosition = entry.IndexOf('.', startIndex); + + if (dotPosition == -1) + { + delimiterPosition = bracketPosition; + } + else if (bracketPosition == -1) + { + delimiterPosition = dotPosition; + } + else + { + delimiterPosition = Math.Min(dotPosition, bracketPosition); + } + + return delimiterPosition; + } + /// /// Convert an ICollection to an array, removing null values. Fast path for case where /// there are no null values. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs index b20d8c46f8..f7d13df7ce 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/DictionaryModelBinderTest.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if DNX451 +using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; using Moq; @@ -64,6 +67,200 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test Assert.Equal("eighty-four", dictionary[84]); } + // modelName, keyFormat, dictionary + public static TheoryData> StringToStringData + { + get + { + var dictionaryWithOne = new Dictionary(StringComparer.Ordinal) + { + { "one", "one" }, + }; + var dictionaryWithThree = new Dictionary(StringComparer.Ordinal) + { + { "one", "one" }, + { "two", "two" }, + { "three", "three" }, + }; + + return new TheoryData> + { + { string.Empty, "[{0}]", dictionaryWithOne }, + { string.Empty, "[{0}]", dictionaryWithThree }, + { "prefix", "prefix[{0}]", dictionaryWithOne }, + { "prefix", "prefix[{0}]", dictionaryWithThree }, + { "prefix.property", "prefix.property[{0}]", dictionaryWithOne }, + { "prefix.property", "prefix.property[{0}]", dictionaryWithThree }, + }; + } + } + + [Theory] + [MemberData(nameof(StringToStringData))] + public async Task BindModel_FallsBackToBindingValues( + string modelName, + string keyFormat, + IDictionary dictionary) + { + // Arrange + var binder = new DictionaryModelBinder(); + var context = CreateContext(); + context.ModelName = modelName; + context.OperationBindingContext.ModelBinder = CreateCompositeBinder(); + context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider(keyFormat, dictionary); + context.ValueProvider = context.OperationBindingContext.ValueProvider; + + var metadataProvider = context.OperationBindingContext.MetadataProvider; + context.ModelMetadata = metadataProvider.GetMetadataForProperty( + typeof(ModelWithDictionaryProperty), + nameof(ModelWithDictionaryProperty.DictionaryProperty)); + + // Act + var result = await binder.BindModelAsync(context); + + // Assert + Assert.NotNull(result); + Assert.False(result.IsFatalError); + Assert.True(result.IsModelSet); + Assert.Equal(modelName, result.Key); + Assert.NotNull(result.ValidationNode); + + var resultDictionary = Assert.IsAssignableFrom>(result.Model); + Assert.Equal(dictionary, resultDictionary); + } + + // Similar to one BindModel_FallsBackToBindingValues case but without an IEnumerableValueProvider. + [Fact] + public async Task BindModel_DoesNotFallBack_WithoutEnumerableValueProvider() + { + // Arrange + var dictionary = new Dictionary(StringComparer.Ordinal) + { + { "one", "one" }, + { "two", "two" }, + { "three", "three" }, + }; + + var binder = new DictionaryModelBinder(); + var context = CreateContext(); + context.ModelName = "prefix"; + context.OperationBindingContext.ModelBinder = CreateCompositeBinder(); + context.OperationBindingContext.ValueProvider = CreateTestValueProvider("prefix[{0}]", dictionary); + context.ValueProvider = context.OperationBindingContext.ValueProvider; + + var metadataProvider = context.OperationBindingContext.MetadataProvider; + context.ModelMetadata = metadataProvider.GetMetadataForProperty( + typeof(ModelWithDictionaryProperty), + nameof(ModelWithDictionaryProperty.DictionaryProperty)); + + // Act + var result = await binder.BindModelAsync(context); + + // Assert + Assert.NotNull(result); + Assert.False(result.IsFatalError); + Assert.True(result.IsModelSet); + Assert.Equal("prefix", result.Key); + Assert.NotNull(result.ValidationNode); + + var resultDictionary = Assert.IsAssignableFrom>(result.Model); + Assert.Empty(resultDictionary); + } + + public static TheoryData> LongToIntData + { + get + { + var dictionaryWithOne = new Dictionary + { + { 0L, 0 }, + }; + var dictionaryWithThree = new Dictionary + { + { -1L, -1 }, + { long.MaxValue, int.MaxValue }, + { long.MinValue, int.MinValue }, + }; + + return new TheoryData> { dictionaryWithOne, dictionaryWithThree }; + } + } + + [Theory] + [MemberData(nameof(LongToIntData))] + public async Task BindModel_FallsBackToBindingValues_WithValueTypes(IDictionary dictionary) + { + // Arrange + var stringDictionary = dictionary.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.ToString()); + var binder = new DictionaryModelBinder(); + var context = CreateContext(); + context.ModelName = "prefix"; + context.OperationBindingContext.ModelBinder = CreateCompositeBinder(); + context.OperationBindingContext.ValueProvider = + CreateEnumerableValueProvider("prefix[{0}]", stringDictionary); + context.ValueProvider = context.OperationBindingContext.ValueProvider; + + var metadataProvider = context.OperationBindingContext.MetadataProvider; + context.ModelMetadata = metadataProvider.GetMetadataForProperty( + typeof(ModelWithDictionaryProperty), + nameof(ModelWithDictionaryProperty.DictionaryProperty)); + + // Act + var result = await binder.BindModelAsync(context); + + // Assert + Assert.NotNull(result); + Assert.False(result.IsFatalError); + Assert.True(result.IsModelSet); + Assert.Equal("prefix", result.Key); + Assert.NotNull(result.ValidationNode); + + var resultDictionary = Assert.IsAssignableFrom>(result.Model); + Assert.Equal(dictionary, resultDictionary); + } + + [Fact] + public async Task BindModel_FallsBackToBindingValues_WithComplexValues() + { + // Arrange + var dictionary = new Dictionary + { + { 23, new ModelWithProperties { Id = 43, Name = "Wilma" } }, + { 27, new ModelWithProperties { Id = 98, Name = "Fred" } }, + }; + var stringDictionary = new Dictionary + { + { "prefix[23].Id", "43" }, + { "prefix[23].Name", "Wilma" }, + { "prefix[27].Id", "98" }, + { "prefix[27].Name", "Fred" }, + }; + var binder = new DictionaryModelBinder(); + var context = CreateContext(); + context.ModelName = "prefix"; + context.OperationBindingContext.ModelBinder = CreateCompositeBinder(); + context.OperationBindingContext.ValueProvider = CreateEnumerableValueProvider("{0}", stringDictionary); + context.ValueProvider = context.OperationBindingContext.ValueProvider; + + var metadataProvider = context.OperationBindingContext.MetadataProvider; + context.ModelMetadata = metadataProvider.GetMetadataForProperty( + typeof(ModelWithDictionaryProperty), + nameof(ModelWithDictionaryProperty.DictionaryProperty)); + + // Act + var result = await binder.BindModelAsync(context); + + // Assert + Assert.NotNull(result); + Assert.False(result.IsFatalError); + Assert.True(result.IsModelSet); + Assert.Equal("prefix", result.Key); + Assert.NotNull(result.ValidationNode); + + var resultDictionary = Assert.IsAssignableFrom>(result.Model); + Assert.Equal(dictionary, resultDictionary); + } + [Fact] public async Task DictionaryModelBinder_DoesNotCreateCollection_IfIsTopLevelObjectAndIsFirstChanceBinding() { @@ -161,6 +358,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test return modelBindingContext; } + private static IModelBinder CreateCompositeBinder() + { + var binders = new IModelBinder[] + { + new TypeConverterModelBinder(), + new TypeMatchModelBinder(), + new MutableObjectModelBinder(), + new ComplexModelDtoModelBinder(), + }; + + return new CompositeModelBinder(binders); + } + + private static IValueProvider CreateEnumerableValueProvider( + string keyFormat, + IDictionary dictionary) + { + // Convert to an IDictionary then wrap it up. + var backingStore = dictionary.ToDictionary( + kvp => string.Format(keyFormat, kvp.Key), + kvp => new[] { kvp.Value }); + var stringCollection = new ReadableStringCollection(backingStore); + + return new ReadableStringCollectionValueProvider( + BindingSource.Form, + stringCollection, + CultureInfo.InvariantCulture); + } + + // Like CreateEnumerableValueProvider except returned instance does not implement IEnumerableValueProvider. + private static IValueProvider CreateTestValueProvider(string keyFormat, IDictionary dictionary) + { + // Convert to an IDictionary then wrap it up. + var backingStore = dictionary.ToDictionary( + kvp => string.Format(keyFormat, kvp.Key), + kvp => (object)kvp.Value); + + return new TestValueProvider(BindingSource.Form, backingStore); + } + private static ModelBindingContext GetModelBindingContext(bool isReadOnly) { var metadataProvider = new TestModelMetadataProvider(); @@ -208,6 +445,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { public Dictionary DictionaryProperty { get; set; } } + + private class ModelWithProperties + { + public int Id { get; set; } + + public string Name { get; set; } + + public override bool Equals(object obj) + { + var other = obj as ModelWithProperties; + return other != null && + Id == other.Id && + string.Equals(Name, other.Name, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + int nameCode = Name == null ? 0 : Name.GetHashCode(); + return nameCode ^ Id.GetHashCode(); + } + + public override string ToString() + { + return $"{{{ Id }, '{ Name }'}}"; + } + } } } #endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs index c3cef91d33..a8d51dc2ad 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs @@ -16,10 +16,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test private static readonly IReadableStringCollection _backingStore = new ReadableStringCollection( new Dictionary { - { "foo", new[] { "fooValue1", "fooValue2"} }, - { "bar.baz", new[] {"someOtherValue" } }, + { "some", new[] { "someValue1", "someValue2" } }, { "null_value", null }, - { "prefix.null_value", null } + { "prefix.name", new[] { "someOtherValue" } }, + { "prefix.null_value", null }, + { "prefix.property1.property", null }, + { "prefix.property2[index]", null }, + { "prefix[index1]", null }, + { "prefix[index1].property1", null }, + { "prefix[index1].property2", null }, + { "prefix[index2].property", null }, + { "[index]", null }, + { "[index].property", null }, + { "[index][anotherIndex]", null }, }); [Fact] @@ -30,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, null); // Act - var result = await valueProvider.ContainsPrefixAsync(""); + var result = await valueProvider.ContainsPrefixAsync(string.Empty); // Assert Assert.False(result); @@ -43,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); // Act - var result = await valueProvider.ContainsPrefixAsync(""); + var result = await valueProvider.ContainsPrefixAsync(string.Empty); // Assert Assert.True(result); @@ -56,9 +65,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); // Act & Assert - Assert.True(await valueProvider.ContainsPrefixAsync("foo")); - Assert.True(await valueProvider.ContainsPrefixAsync("bar")); - Assert.True(await valueProvider.ContainsPrefixAsync("bar.baz")); + Assert.True(await valueProvider.ContainsPrefixAsync("some")); + Assert.True(await valueProvider.ContainsPrefixAsync("prefix")); + Assert.True(await valueProvider.ContainsPrefixAsync("prefix.name")); + Assert.True(await valueProvider.ContainsPrefixAsync("[index]")); + Assert.True(await valueProvider.ContainsPrefixAsync("prefix[index1]")); } [Fact] @@ -80,15 +91,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test // Arrange var expected = new Dictionary { - { "bar", "bar" }, - { "foo", "foo" }, + { "index", "[index]" }, { "null_value", "null_value" }, - { "prefix", "prefix" } + { "prefix", "prefix" }, + { "some", "some" }, }; var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture: null); // Act - var result = await valueProvider.GetKeysFromPrefixAsync(""); + var result = await valueProvider.GetKeysFromPrefixAsync(string.Empty); // Assert Assert.Equal(expected, result.OrderBy(kvp => kvp.Key)); @@ -111,15 +122,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test public async Task GetKeysFromPrefixAsync_KnownPrefix_ReturnsMatchingItems() { // Arrange + var expected = new Dictionary + { + { "name", "prefix.name" }, + { "null_value", "prefix.null_value" }, + { "property1", "prefix.property1" }, + { "property2", "prefix.property2" }, + { "index1", "prefix[index1]" }, + { "index2", "prefix[index2]" }, + }; var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); // Act - var result = await valueProvider.GetKeysFromPrefixAsync("bar"); + var result = await valueProvider.GetKeysFromPrefixAsync("prefix"); // Assert - var kvp = Assert.Single(result); - Assert.Equal("baz", kvp.Key); - Assert.Equal("bar.baz", kvp.Value); + Assert.Equal(expected, result); + } + + [Fact] + public async Task GetKeysFromPrefixAsync_IndexPrefix_ReturnsMatchingItems() + { + // Arrange + var expected = new Dictionary + { + { "property", "[index].property" }, + { "anotherIndex", "[index][anotherIndex]" } + }; + var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); + + // Act + var result = await valueProvider.GetKeysFromPrefixAsync("[index]"); + + // Assert + Assert.Equal(expected, result); } [Fact] @@ -130,13 +166,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture); // Act - var vpResult = await valueProvider.GetValueAsync("bar.baz"); + var result = await valueProvider.GetValueAsync("prefix.name"); // Assert - Assert.NotNull(vpResult); - Assert.Equal("someOtherValue", vpResult.RawValue); - Assert.Equal("someOtherValue", vpResult.AttemptedValue); - Assert.Equal(culture, vpResult.Culture); + Assert.NotNull(result); + Assert.Equal("someOtherValue", result.RawValue); + Assert.Equal("someOtherValue", result.AttemptedValue); + Assert.Equal(culture, result.Culture); } [Fact] @@ -147,13 +183,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture); // Act - var vpResult = await valueProvider.GetValueAsync("foo"); + var result = await valueProvider.GetValueAsync("some"); // Assert - Assert.NotNull(vpResult); - Assert.Equal(new[] { "fooValue1", "fooValue2" }, (IList)vpResult.RawValue); - Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue); - Assert.Equal(culture, vpResult.Culture); + Assert.NotNull(result); + Assert.Equal(new[] { "someValue1", "someValue2" }, (IList)result.RawValue); + Assert.Equal("someValue1,someValue2", result.AttemptedValue); + Assert.Equal(culture, result.Culture); } [Theory] @@ -185,11 +221,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, culture); // Act - var vpResult = await valueProvider.GetValueAsync("key"); + var result = await valueProvider.GetValueAsync("key"); // Assert - Assert.Equal(new[] { null, null, "value" }, vpResult.RawValue as IEnumerable); - Assert.Equal(",,value", vpResult.AttemptedValue); + Assert.Equal(new[] { null, null, "value" }, result.RawValue as IEnumerable); + Assert.Equal(",,value", result.AttemptedValue); } [Fact] @@ -199,10 +235,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); // Act - var vpResult = await valueProvider.GetValueAsync("bar"); + var result = await valueProvider.GetValueAsync("prefix"); // Assert - Assert.Null(vpResult); + Assert.Null(result); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoundTripTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoundTripTests.cs index 5195cfa7d4..d834e043ae 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoundTripTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoundTripTests.cs @@ -101,13 +101,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); var client = server.CreateClient(); + var expected = "6 feet"; // Act var expression = await client.GetStringAsync("http://localhost/RoundTrip/GetPersonParentHeightAttribute"); + var keyValuePairs = new[] + { + new KeyValuePair(expression, expected), + }; + var result = await GetPerson(client, keyValuePairs); // Assert Assert.Equal("Parent.Attributes[height]", expression); - // TODO: https://github.com/aspnet/Mvc/issues/1418 Requires resolution in model binding + Assert.Equal(expected, result.Parent.Attributes["height"]); } // Uses the expression p => p.Dependents[0].Dependents[0].Name diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs index bec4e95844..e60eb1aacd 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; @@ -14,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests public class DictionaryModelBinderIntegrationTest { [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefix_Success() + public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndKVP_Success() { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -55,7 +54,48 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests } [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success() + public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithPrefixAndItem_Success() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Dictionary) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + request.QueryString = new QueryString("?parameter[key0]=10"); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType>(modelBindingResult.Model); + Assert.Equal(new Dictionary() { { "key0", 10 } }, model); + + Assert.Equal(0, modelState.ErrorCount); + Assert.True(modelState.IsValid); + + var kvp = Assert.Single(modelState); + Assert.Equal("parameter[key0]", kvp.Key); + var entry = kvp.Value; + Assert.Equal("10", entry.Value.AttemptedValue); + Assert.Equal("10", entry.Value.RawValue); + } + + [Theory] + [InlineData("?prefix[key0]=10")] + [InlineData("?prefix[0].Key=key0&prefix[0].Value=10")] + public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success( + string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -71,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { - request.QueryString = new QueryString("?prefix[0].Key=key0&prefix[0].Value=10"); + request.QueryString = new QueryString(queryString); }); var modelState = new ModelStateDictionary(); @@ -86,21 +126,15 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new Dictionary() { { "key0", 10 }, }, model); - Assert.Equal(2, modelState.Count); + Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); - - var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Key").Value; - Assert.Equal("key0", entry.Value.AttemptedValue); - Assert.Equal("key0", entry.Value.RawValue); - - entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Value").Value; - Assert.Equal("10", entry.Value.AttemptedValue); - Assert.Equal("10", entry.Value.RawValue); } - [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success() + [Theory] + [InlineData("?[key0]=10")] + [InlineData("?[0].Key=key0&[0].Value=10")] + public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success(string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -112,7 +146,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { - request.QueryString = new QueryString("?[0].Key=key0&[0].Value=10"); + request.QueryString = new QueryString(queryString); }); var modelState = new ModelStateDictionary(); @@ -127,17 +161,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var model = Assert.IsType>(modelBindingResult.Model); Assert.Equal(new Dictionary() { { "key0", 10 }, }, model); - Assert.Equal(2, modelState.Count); + Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); - - var entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Key").Value; - Assert.Equal("key0", entry.Value.AttemptedValue); - Assert.Equal("key0", entry.Value.RawValue); - - entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Value").Value; - Assert.Equal("10", entry.Value.AttemptedValue); - Assert.Equal("10", entry.Value.RawValue); } [Fact] @@ -164,9 +190,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests // Assert Assert.NotNull(modelBindingResult); Assert.True(modelBindingResult.IsModelSet); - Assert.Empty(Assert.IsType>(modelBindingResult.Model)); - Assert.Equal(0, modelState.Count); + var model = Assert.IsType>(modelBindingResult.Model); + Assert.Empty(model); + + Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); } @@ -174,10 +202,29 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests private class Person { public int Id { get; set; } + + public override bool Equals(object obj) + { + var other = obj as Person; + + return other != null && Id == other.Id; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public override string ToString() + { + return $"{{ { Id } }}"; + } } - [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithPrefix_Success() + [Theory] + [InlineData("?parameter[key0].Id=10")] + [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=10")] + public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithPrefix_Success(string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -189,7 +236,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { - request.QueryString = new QueryString("?parameter[0].Key=key0¶meter[0].Value.Id=10"); + request.QueryString = new QueryString(queryString); }); var modelState = new ModelStateDictionary(); @@ -202,25 +249,18 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); - Assert.Equal(1, model.Count); - Assert.Equal("key0", model.Keys.First()); - Assert.Equal(model.Values, model.Values); + Assert.Equal(new Dictionary { { "key0", new Person { Id = 10 } }, }, model); - Assert.Equal(2, modelState.Count); + Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); - - var entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Key").Value; - Assert.Equal("key0", entry.Value.AttemptedValue); - Assert.Equal("key0", entry.Value.RawValue); - - entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[0].Value.Id").Value; - Assert.Equal("10", entry.Value.AttemptedValue); - Assert.Equal("10", entry.Value.RawValue); } - [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefix_Success() + [Theory] + [InlineData("?prefix[key0].Id=10")] + [InlineData("?prefix[0].Key=key0&prefix[0].Value.Id=10")] + public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_WithExplicitPrefix_Success( + string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -236,7 +276,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { - request.QueryString = new QueryString("?prefix[0].Key=key0&prefix[0].Value.Id=10"); + request.QueryString = new QueryString(queryString); }); var modelState = new ModelStateDictionary(); @@ -249,25 +289,17 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); - Assert.Equal(1, model.Count); - Assert.Equal("key0", model.Keys.First()); - Assert.Equal(model.Values, model.Values); + Assert.Equal(new Dictionary { { "key0", new Person { Id = 10 } }, }, model); - Assert.Equal(2, modelState.Count); + Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); - - var entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Key").Value; - Assert.Equal("key0", entry.Value.AttemptedValue); - Assert.Equal("key0", entry.Value.RawValue); - - entry = Assert.Single(modelState, kvp => kvp.Key == "prefix[0].Value.Id").Value; - Assert.Equal("10", entry.Value.AttemptedValue); - Assert.Equal("10", entry.Value.RawValue); } - [Fact] - public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_EmptyPrefix_Success() + [Theory] + [InlineData("?[key0].Id=10")] + [InlineData("?[0].Key=key0&[0].Value.Id=10")] + public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_EmptyPrefix_Success(string queryString) { // Arrange var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); @@ -279,7 +311,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { - request.QueryString = new QueryString("?[0].Key=key0&[0].Value.Id=10"); + request.QueryString = new QueryString(queryString); }); var modelState = new ModelStateDictionary(); @@ -292,21 +324,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.True(modelBindingResult.IsModelSet); var model = Assert.IsType>(modelBindingResult.Model); - Assert.Equal(1, model.Count); - Assert.Equal("key0", model.Keys.First()); - Assert.Equal(model.Values, model.Values); + Assert.Equal(new Dictionary { { "key0", new Person { Id = 10 } }, }, model); - Assert.Equal(2, modelState.Count); + Assert.NotEmpty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); - - var entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Key").Value; - Assert.Equal("key0", entry.Value.AttemptedValue); - Assert.Equal("key0", entry.Value.RawValue); - - entry = Assert.Single(modelState, kvp => kvp.Key == "[0].Value.Id").Value; - Assert.Equal("10", entry.Value.AttemptedValue); - Assert.Equal("10", entry.Value.RawValue); } [Fact] @@ -333,9 +355,11 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests // Assert Assert.NotNull(modelBindingResult); Assert.True(modelBindingResult.IsModelSet); - Assert.Empty(Assert.IsType>(modelBindingResult.Model)); - Assert.Equal(0, modelState.Count); + var model = Assert.IsType>(modelBindingResult.Model); + Assert.Empty(model); + + Assert.Empty(modelState); Assert.Equal(0, modelState.ErrorCount); Assert.True(modelState.IsValid); }