From 390ebbb258215137a1cb493d2170eda9249d4afe Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 13 Mar 2018 22:14:03 -0700 Subject: [PATCH] Do not bind `"[index]"` in `CollectionModelBinder` subsetting feature - #7091 - add `IKeyRewriterValueProvider` to remove rewritten keys or value providers containing such keys - similar to `IBindingSourceValueProvider` except `CompositeValueProvider` keeps non-implementers around - remove `after.Order == before.Order` special cases - a premature optimization that could lead to lost inner provider replacements - rework `EnumerableValueProviderTest` to ease test override in `CompositeValueProviderTest` - add `EmptyValueProvider` fields to reduce `CompositeValueProvider` allocations nits: - remove Linq use in `CompositeValueProvider` - do not create an unnecessary dictionary in `CompositeValueProvider.Filter(...)` methods - accept VS suggestions, mostly pattern matching --- .../Internal/DefaultModelBindingContext.cs | 7 +- .../Binders/CollectionModelBinder.cs | 11 +- .../ModelBinding/CompositeValueProvider.cs | 88 +++++++- .../ModelBinding/IKeyRewriterValueProvider.cs | 23 +++ .../ModelBinding/JQueryValueProvider.cs | 18 +- .../CompositeValueProviderTest.cs | 130 +++++++++++- .../EnumerableValueProviderTest.cs | 32 +-- .../JQueryFormValueProviderTest.cs | 15 ++ .../JQueryQueryStringValueProviderTest.cs | 18 ++ .../CollectionModelBinderIntegrationTest.cs | 9 +- .../DictionaryModelBinderIntegrationTest.cs | 194 +++++++++++++++++- 11 files changed, 506 insertions(+), 39 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IKeyRewriterValueProvider.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs index 5648576fd6..0ff9292a02 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelBindingContext.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public class DefaultModelBindingContext : ModelBindingContext { + private static readonly IValueProvider EmptyValueProvider = new CompositeValueProvider(); + private IValueProvider _originalValueProvider; private ActionContext _actionContext; private ModelStateDictionary _modelState; @@ -314,13 +316,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return valueProvider; } - var bindingSourceValueProvider = valueProvider as IBindingSourceValueProvider; - if (bindingSourceValueProvider == null) + if (!(valueProvider is IBindingSourceValueProvider bindingSourceValueProvider)) { return valueProvider; } - return bindingSourceValueProvider.Filter(bindingSource) ?? new CompositeValueProvider(); + return bindingSourceValueProvider.Filter(bindingSource) ?? EmptyValueProvider; } private struct State diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs index d219519e4f..16834ebadc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/CollectionModelBinder.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders /// Type of elements in the collection. public class CollectionModelBinder : ICollectionModelBinder { + private static readonly IValueProvider EmptyValueProvider = new CompositeValueProvider(); private Func _modelCreator; /// @@ -244,7 +245,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Logger.AttemptingToBindCollectionUsingIndices(bindingContext); var indexPropertyName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "index"); - var valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName); + + // Remove any value provider that may not use indexPropertyName as-is. Don't match e.g. Model[index]. + var valueProvider = bindingContext.ValueProvider; + if (valueProvider is IKeyRewriterValueProvider keyRewriterValueProvider) + { + valueProvider = keyRewriterValueProvider.Filter() ?? EmptyValueProvider; + } + + var valueProviderResultIndex = valueProvider.GetValue(indexPropertyName); var indexNames = GetIndexNamesFromValueProviderResult(valueProviderResultIndex); return BindComplexCollectionFromIndexes(bindingContext, indexNames); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeValueProvider.cs index 9faf5c38c3..eaf15df1be 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeValueProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/CompositeValueProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -15,7 +14,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public class CompositeValueProvider : Collection, IEnumerableValueProvider, - IBindingSourceValueProvider + IBindingSourceValueProvider, + IKeyRewriterValueProvider { /// /// Initializes a new instance of . @@ -117,8 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { foreach (var valueProvider in this) { - var enumeratedProvider = valueProvider as IEnumerableValueProvider; - if (enumeratedProvider != null) + if (valueProvider is IEnumerableValueProvider enumeratedProvider) { var result = enumeratedProvider.GetKeysFromPrefix(prefix); if (result != null && result.Count > 0) @@ -160,13 +159,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(bindingSource)); } - var filteredValueProviders = new List(); - foreach (var valueProvider in this.OfType()) + var shouldFilter = false; + for (var i = 0; i < Count; i++) { - var result = valueProvider.Filter(bindingSource); - if (result != null) + var valueProvider = Items[i]; + if (valueProvider is IBindingSourceValueProvider) { - filteredValueProviders.Add(result); + shouldFilter = true; + break; + } + } + + if (!shouldFilter) + { + // No inner IBindingSourceValueProvider implementations. Result will be empty. + return null; + } + + var filteredValueProviders = new List(); + for (var i = 0; i < Count; i++) + { + var valueProvider = Items[i]; + if (valueProvider is IBindingSourceValueProvider bindingSourceValueProvider) + { + var result = bindingSourceValueProvider.Filter(bindingSource); + if (result != null) + { + filteredValueProviders.Add(result); + } } } @@ -176,12 +196,58 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return null; } - if (filteredValueProviders.Count == Count) + return new CompositeValueProvider(filteredValueProviders); + } + + /// + /// + /// Value providers are included by default. If a contained does not implement + /// , will not remove it. + /// + public IValueProvider Filter() + { + var shouldFilter = false; + for (var i = 0; i < Count; i++) { - // No need for a new CompositeValueProvider. + var valueProvider = Items[i]; + if (valueProvider is IKeyRewriterValueProvider) + { + shouldFilter = true; + break; + } + } + + if (!shouldFilter) + { + // No inner IKeyRewriterValueProvider implementations. Nothing to exclude. return this; } + var filteredValueProviders = new List(); + for (var i = 0; i < Count; i++) + { + var valueProvider = Items[i]; + if (valueProvider is IKeyRewriterValueProvider keyRewriterValueProvider) + { + var result = keyRewriterValueProvider.Filter(); + if (result != null) + { + filteredValueProviders.Add(result); + } + } + else + { + // Assume value providers that aren't rewriter-aware do not rewrite their keys. + filteredValueProviders.Add(valueProvider); + } + } + + if (filteredValueProviders.Count == 0) + { + // Do not create an empty CompositeValueProvider. + return null; + } + return new CompositeValueProvider(filteredValueProviders); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IKeyRewriterValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IKeyRewriterValueProvider.cs new file mode 100644 index 0000000000..f2bb03cdb7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/IKeyRewriterValueProvider.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// A value provider which can filter its contents to remove keys rewritten compared to the request data. + /// + public interface IKeyRewriterValueProvider : IValueProvider + { + /// + /// Filters the value provider to remove keys rewritten compared to the request data. + /// + /// + /// If the request contains values with keys Model.Property and Collection[index], the returned + /// will not match Model[Property] or Collection.index. + /// + /// + /// The filtered value provider or if the value provider only contains rewritten keys. + /// + IValueProvider Filter(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs index a2ffca0f2a..659a943934 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs @@ -10,9 +10,12 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ModelBinding { /// - /// An for jQuery formatted form data. + /// An for jQuery formatted data. /// - public abstract class JQueryValueProvider : BindingSourceValueProvider, IEnumerableValueProvider + public abstract class JQueryValueProvider : + BindingSourceValueProvider, + IEnumerableValueProvider, + IKeyRewriterValueProvider { private readonly IDictionary _values; private PrefixContainer _prefixContainer; @@ -85,5 +88,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return ValueProviderResult.None; } + + /// + /// + /// Always returns because creates this + /// with rewritten keys (if original contains brackets) or duplicate keys + /// (that will match). + /// + public IValueProvider Filter() + { + return null; + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs index 86e6be22d9..0da5681ef6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -12,6 +14,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { public class CompositeValueProviderTest : EnumerableValueProviderTest { + [Fact] + public override void FilterInclude() + { + // Arrange + var provider = GetBindingSourceValueProvider(BindingSource.Query, BackingStore, culture: null); + var originalProviders = ((CompositeValueProvider)provider).ToArray(); + var bindingSource = new BindingSource( + BindingSource.Query.Id, + displayName: null, + isGreedy: true, + isFromRequest: true); + + // Act + var result = provider.Filter(bindingSource); + + // Assert (does not change inner providers) + var newProvider = Assert.IsType(result); + Assert.Equal(originalProviders, newProvider, ReferenceEqualityComparer.Instance); + } + protected override IEnumerableValueProvider GetEnumerableValueProvider( BindingSource bindingSource, Dictionary values, @@ -96,7 +118,113 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Same(valueProvider1.Object, filteredProvider); } - private Mock GetMockValueProvider(string bindingSourceId) + public static TheoryData Filter_ReturnsProviderData + { + get + { + // None filter themselves out. + var noneRewrite = new[] + { + GetValueProvider(rewritesKeys: false), + GetValueProvider(rewritesKeys: false), + }; + // None implement IKeyRewriterValueProvider. + var noneImplement = new[] { GetMockValueProvider("One").Object, GetMockValueProvider("Two").Object }; + + return new TheoryData + { + // Starts empty + new CompositeValueProvider(), + + new CompositeValueProvider(noneRewrite), + new CompositeValueProvider(noneImplement), + }; + } + } + + [Theory] + [MemberData(nameof(Filter_ReturnsProviderData))] + public void Filter_ReturnsProvider(CompositeValueProvider provider) + { + // Arrange + var originalProviders = provider.ToArray(); + + // Act + var result = provider.Filter(); + + // Assert (does not change inner providers) + var newProvider = Assert.IsType(result); + Assert.Equal(originalProviders, newProvider, ReferenceEqualityComparer.Instance); + } + + [Fact] + public void Filter_ReturnsNull() + { + // Arrange + var allRewrite = new[] { GetValueProvider(rewritesKeys: true), GetValueProvider(rewritesKeys: true) }; + var provider = new CompositeValueProvider(allRewrite); + + // Act + var result = provider.Filter(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Filter_RemovesThoseThatRewrite() + { + // Arrange + var doesNotRewrite1 = GetValueProvider(rewritesKeys: false); + var doesNotRewrite2 = GetValueProvider(rewritesKeys: false); + var doesNotImplement1 = GetMockValueProvider("One").Object; + var doesNotImplement2 = GetMockValueProvider("Two").Object; + var rewrites1 = GetValueProvider(rewritesKeys: true); + var rewrites2 = GetValueProvider(rewritesKeys: true); + var providers = new IValueProvider[] + { + doesNotRewrite1, + doesNotImplement1, + rewrites1, + doesNotRewrite2, + doesNotImplement2, + rewrites2, + }; + var expectedProviders = new IValueProvider[] + { + doesNotRewrite1, + doesNotImplement1, + doesNotRewrite2, + doesNotImplement2, + }; + + var provider = new CompositeValueProvider(providers); + + // Act + var result = provider.Filter(); + + // Assert + Assert.NotSame(provider, result); + var newProvider = Assert.IsType(result); + Assert.Equal(expectedProviders, newProvider, ReferenceEqualityComparer.Instance); + } + + private static IKeyRewriterValueProvider GetValueProvider(bool rewritesKeys) + { + var valueProvider = new Mock(MockBehavior.Strict); + if (rewritesKeys) + { + valueProvider.Setup(vp => vp.Filter()).Returns(null); + } + else + { + valueProvider.Setup(vp => vp.Filter()).Returns(valueProvider.Object); + } + + return valueProvider.Object; + } + + private static Mock GetMockValueProvider(string bindingSourceId) { var valueProvider = new Mock(MockBehavior.Strict); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs index 8b01f266b6..19c3d37407 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { public abstract class EnumerableValueProviderTest { - private static readonly Dictionary _backingStore = new Dictionary + protected static Dictionary BackingStore { get; } = new Dictionary { { "some", new[] { "someValue1", "someValue2" } }, { "null_value", StringValues.Empty }, @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix() { // Arrange - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.ContainsPrefix(string.Empty); @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes(string prefix) { // Arrange - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act & Assert Assert.True(valueProvider.ContainsPrefix(prefix)); @@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ContainsPrefix_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix() { // Arrange - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.ContainsPrefix("biff"); @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { "prefix", "prefix" }, { "some", "some" }, }; - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.GetKeysFromPrefix(string.Empty); @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void GetKeysFromPrefix_UnknownPrefix_ReturnsEmptyDictionary() { // Arrange - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.GetKeysFromPrefix("abc"); @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { "index1", "prefix[index1]" }, { "index2", "prefix[index2]" }, }; - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.GetKeysFromPrefix("prefix"); @@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { "property", "[index].property" }, { "anotherIndex", "[index][anotherIndex]" } }; - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.GetKeysFromPrefix("[index]"); @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var culture = new CultureInfo("fr-FR"); - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture); // Act var result = valueProvider.GetValue("prefix.name"); @@ -176,7 +176,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var culture = new CultureInfo("fr-FR"); - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture); // Act var result = valueProvider.GetValue("some"); @@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { // Arrange var culture = new CultureInfo("fr-FR"); - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture); // Act var result = valueProvider.GetValue(key); @@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void GetValue_ReturnsNullIfKeyNotFound() { // Arrange - var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, BackingStore, culture: null); // Act var result = valueProvider.GetValue("prefix"); @@ -236,10 +236,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } [Fact] - public void FilterInclude() + public virtual void FilterInclude() { // Arrange - var provider = GetBindingSourceValueProvider(BindingSource.Query, _backingStore, culture: null); + var provider = GetBindingSourceValueProvider(BindingSource.Query, BackingStore, culture: null); var bindingSource = new BindingSource( BindingSource.Query.Id, @@ -259,7 +259,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void FilterExclude() { // Arrange - var provider = GetBindingSourceValueProvider(BindingSource.Query, _backingStore, culture: null); + var provider = GetBindingSourceValueProvider(BindingSource.Query, BackingStore, culture: null); var bindingSource = new BindingSource( "Test", @@ -274,7 +274,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.Null(result); } - private IBindingSourceValueProvider GetBindingSourceValueProvider( + protected IBindingSourceValueProvider GetBindingSourceValueProvider( BindingSource bindingSource, Dictionary values, CultureInfo culture) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs index 3dc15a4570..608180ac3b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Microsoft.Extensions.Primitives; +using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding { @@ -16,5 +17,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { return new JQueryFormValueProvider(bindingSource, values, culture); } + + [Fact] + public void Filter_ExcludesItself() + { + // Arrange + var dictionary = new Dictionary(); + var provider = new JQueryFormValueProvider(BindingSource.Form, dictionary, CultureInfo.CurrentCulture); + + // Act + var result = provider.Filter(); + + // Assert + Assert.Null(result); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs index cb8453fd6b..830ceac1ab 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Microsoft.Extensions.Primitives; +using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding { @@ -16,5 +17,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { return new JQueryQueryStringValueProvider(bindingSource, values, culture); } + + [Fact] + public void Filter_ExcludesItself() + { + // Arrange + var dictionary = new Dictionary(); + var provider = new JQueryQueryStringValueProvider( + BindingSource.Form, + dictionary, + CultureInfo.CurrentCulture); + + // Act + var result = provider.Filter(); + + // Assert + Assert.Null(result); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs index 9cb5b988eb..7cb0c782d2 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs @@ -63,6 +63,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [Theory] [InlineData("?prefix[0]=10&prefix[1]=11")] [InlineData("?prefix.index=low&prefix.index=high&prefix[low]=10&prefix[high]=11")] + [InlineData("?prefix.index=index&prefix.index=indexer&prefix[index]=10&prefix[indexer]=11")] + [InlineData("?prefix.index=index&prefix.index=indexer&prefix[index]=10&prefix[indexer]=11&prefix[extra]=12")] public async Task CollectionModelBinder_BindsListOfSimpleType_WithExplicitPrefix_Success(string queryString) { // Arrange @@ -101,6 +103,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [Theory] [InlineData("?[0]=10&[1]=11")] [InlineData("?index=low&index=high&[high]=11&[low]=10")] + [InlineData("?index=index&index=indexer&[indexer]=11&[index]=10")] + [InlineData("?index=index&index=indexer&[indexer]=11&[index]=10&[extra]=12")] public async Task CollectionModelBinder_BindsCollectionOfSimpleType_EmptyPrefix_Success(string queryString) { // Arrange @@ -172,6 +176,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?index=low&index=high&[low].Id=10&[high].Id=11")] [InlineData("?parameter[0].Id=10¶meter[1].Id=11")] [InlineData("?parameter.index=low¶meter.index=high¶meter[low].Id=10¶meter[high].Id=11")] + [InlineData("?parameter.index=index¶meter.index=indexer¶meter[index].Id=10¶meter[indexer].Id=11")] public async Task CollectionModelBinder_BindsListOfComplexType_ImpliedPrefix_Success(string queryString) { // Arrange @@ -207,6 +212,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [Theory] [InlineData("?prefix[0].Id=10&prefix[1].Id=11")] [InlineData("?prefix.index=low&prefix.index=high&prefix[high].Id=11&prefix[low].Id=10")] + [InlineData("?prefix.index=index&prefix.index=indexer&prefix[indexer].Id=11&prefix[index].Id=10")] public async Task CollectionModelBinder_BindsListOfComplexType_ExplicitPrefix_Success(string queryString) { // Arrange @@ -683,6 +689,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?index=low&[low].Street=LongStreet")] [InlineData("?parameter[0].Street=LongStreet")] [InlineData("?parameter.index=low¶meter[low].Street=LongStreet")] + [InlineData("?parameter.index=index¶meter[index].Street=LongStreet")] public async Task CollectionModelBinder_BindsCollectionOfComplexType_ImpliedPrefix_FindsValidationErrors( string queryString) { @@ -1249,4 +1256,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs index fdc41aacaa..79a77e57f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Mvc.IntegrationTests @@ -141,6 +142,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?prefix[key0]=10")] [InlineData("?prefix[0].Key=key0&prefix[0].Value=10")] [InlineData("?prefix.index=low&prefix[low].Key=key0&prefix[low].Value=10")] + [InlineData("?prefix.index=index&prefix[index].Key=key0&prefix[index].Value=10")] + [InlineData("?prefix.index=index&prefix[index].Key=key0&prefix[index].Value=10&prefix[extra].Key=key4&prefix[extra].Value=5")] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_WithExplicitPrefix_Success( string queryString) { @@ -181,6 +184,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?[key0]=10")] [InlineData("?[0].Key=key0&[0].Value=10")] [InlineData("?index=low&[low].Key=key0&[low].Value=10")] + [InlineData("?index=index&[index].Key=key0&[index].Value=10")] + [InlineData("?index=index&[index].Key=key0&[index].Value=10&[extra].Key=key4&[extra].Value=5")] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_EmptyPrefix_Success(string queryString) { // Arrange @@ -212,6 +217,186 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.True(modelState.IsValid); } + public static TheoryData> ThreeEntryTestData + { + get + { + var impliedPrefixQueryString = "?parameter[archive]=1¶meter[correlation]=2¶meter[index]=3"; + var noPrefixQueryString = "?[archive]=1&[correlation]=2&[index]=3"; + var reversedNoPrefixQueryString = "?[index]=3&[correlation]=2&[archive]=1"; + var impliedPrefixDictionary = new Dictionary + { + { "parameter[archive]", "1" }, + { "parameter[correlation]", "2" }, + { "parameter[index]", "3" }, + }; + var reversedImpliedPrefixDictionary = new Dictionary + { + { "parameter[index]", "3" }, + { "parameter[correlation]", "2" }, + { "parameter[archive]", "1" }, + }; + var longFormDictionary = new Dictionary + { + { "parameter[0].Key", "archive" }, + { "parameter[0].Value", "1" }, + { "parameter[1].Key", "correlation" }, + { "parameter[1].Value", "2" }, + { "parameter[2].Key", "index" }, + { "parameter[2].Value", "3" }, + }; + var longerFormDictionary = new Dictionary + { + { "parameter[indexer].Key", "archive" }, + { "parameter[indexer].Value", "1" }, + { "parameter[index].Key", "correlation" }, + { "parameter.index", new[] { "indexer", "index", "indexes" } }, + { "parameter[index].Value", "2" }, + { "parameter[indexes].Key", "index" }, + { "parameter[indexes].Value", "3" }, + }; + var longestFormDictionary = new Dictionary + { + { "parameter[indexer].Key", "archive" }, + { "parameter[indexer].Value", "1" }, + { "parameter[index].Key", "correlation" }, + { "parameter[extra].Key", "index" }, + { "parameter[extra].Value", "4" }, + { "parameter.index", new[] { "indexer", "index", "indexes" } }, + { "parameter[index].Value", "2" }, + { "parameter[indexes].Key", "index" }, + { "parameter[indexes].Value", "3" }, + { "parameter[another].Key", "index" }, + { "parameter[another].Value", "5" }, + }; + var noPrefixDictionary = new Dictionary + { + { "[archive]", "1" }, + { "[correlation]", "2" }, + { "[index]", "3" }, + }; + var reversedNoPrefixDictionary = new Dictionary + { + { "[index]", "3" }, + { "[correlation]", "2" }, + { "[archive]", "1" }, + }; + + return new TheoryData> + { + request => request.QueryString = new QueryString(impliedPrefixQueryString), + request => request.QueryString = new QueryString(noPrefixQueryString), + request => request.QueryString = new QueryString(reversedNoPrefixQueryString), + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(impliedPrefixDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(reversedImpliedPrefixDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(longFormDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(longerFormDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(longestFormDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(noPrefixDictionary); + }, + request => + { + request.ContentType = "application/x-www-form-urlencoded"; + request.Form = new FormCollection(reversedNoPrefixDictionary); + }, + }; + } + } + + [Theory] + [MemberData(nameof(ThreeEntryTestData))] + public async Task DictionaryModelBinder_Binds3EntriesOfSimpleType(Action updateRequest) + { + // Arrange + var expectedDictionary = new Dictionary + { + { "archive", 1 }, + { "correlation", 2 }, + { "index", 3 }, + }; + + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var testContext = ModelBindingTestHelper.GetTestContext(updateRequest); + var modelState = testContext.ModelState; + var parameter = new ParameterDescriptor + { + Name = "parameter", + ParameterType = typeof(Dictionary), + }; + + // Act + var result = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(result.IsModelSet); + + var model = Assert.IsType>(result.Model); + Assert.Equal(expectedDictionary, model); + + Assert.NotEmpty(modelState); + Assert.True(modelState.IsValid); + } + + [Theory] + [MemberData(nameof(ThreeEntryTestData))] + public async Task DictionaryModelBinder_Binds3EntriesOfSimpleType_WithJQueryQueryString( + Action updateRequest) + { + // Arrange + var expectedDictionary = new Dictionary + { + { "archive", 1 }, + { "correlation", 2 }, + { "index", 3 }, + }; + + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var testContext = ModelBindingTestHelper.GetTestContext( + updateRequest, + options => options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory())); + var modelState = testContext.ModelState; + var parameter = new ParameterDescriptor + { + Name = "parameter", + ParameterType = typeof(Dictionary), + }; + + // Act + var result = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(result.IsModelSet); + + var model = Assert.IsType>(result.Model); + Assert.Equal(expectedDictionary, model); + + Assert.NotEmpty(modelState); + Assert.True(modelState.IsValid); + } + [Fact] public async Task DictionaryModelBinder_BindsDictionaryOfSimpleType_NoData() { @@ -251,9 +436,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public override bool Equals(object obj) { - var other = obj as Person; - - return other != null && Id == other.Id; + return obj is Person other && Id == other.Id; } public override int GetHashCode() @@ -274,6 +457,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?parameter[key0].Id=10")] [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=10")] [InlineData("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=10")] + [InlineData("?parameter.index=index¶meter[index].Key=key0¶meter[index].Value.Id=10")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_Success(string queryString) { // Arrange @@ -309,6 +493,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [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( string queryString) { @@ -352,6 +537,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests [InlineData("?parameter[key0].Id=100")] [InlineData("?parameter[0].Key=key0¶meter[0].Value.Id=100")] [InlineData("?parameter.index=low¶meter[low].Key=key0¶meter[low].Value.Id=100")] + [InlineData("?parameter.index=index¶meter[index].Key=key0¶meter[index].Value.Id=100")] public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_FindsValidationErrors( string queryString) { @@ -757,4 +943,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } } } -} \ No newline at end of file +}