From bda850187d7498fb2b0ddc5beb32e90479d8d4c8 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Mon, 3 Aug 2015 08:00:03 -0700 Subject: [PATCH] Add support for jQuery syntax in form data - #2705 - add `JQueryFormValueProvider` and `JQueryFormValueProviderFactory` - carry some code forward from MVC 5; correct to follow current coding guidelines - refactor `ReadableStringCollectionValueProviderTest` into abstract `EnumerableValueProviderTest` - enables reuse in new `JQueryFormValueProviderTest` - also run these tests in `CompositeValueProviderTest` nits: - do not create a duplicate `CompositeValueProvider` instance in `Filter()` - correct garbled sentence in `IBindingSourceValueProvider` doc comments - simplify `FormValueProviderFactoryTest` (no need for Moq) and correct test name - correct test class / file names - `CompositeValueProviderTests` -> `CompositeValueProviderTest` - `FormValueProviderFactoryTests` -> `FormValueProviderFactoryTest` --- .../ModelBinding/CompositeValueProvider.cs | 12 +- .../IBindingSourceValueProvider.cs | 2 +- .../ModelBinding/JQueryFormValueProvider.cs | 114 +++++++ .../JQueryFormValueProviderFactory.cs | 113 +++++++ .../MvcCoreMvcOptionsSetup.cs | 1 + .../Properties/Resources.Designer.cs | 16 + src/Microsoft.AspNet.Mvc.Core/Resources.resx | 57 ++-- ...Tests.cs => CompositeValueProviderTest.cs} | 30 +- .../EnumerableValueProviderTest.cs | 302 ++++++++++++++++++ ...sts.cs => FormValueProviderFactoryTest.cs} | 24 +- .../JQueryFormValueProviderFactoryTest.cs | 135 ++++++++ .../JQueryFormValueProviderTest.cs | 19 ++ ...adableStringCollectionValueProviderTest.cs | 280 +--------------- ...TypeConverterModelBinderIntegrationTest.cs | 102 +++++- .../MvcOptionsSetupTest.cs | 5 +- 15 files changed, 874 insertions(+), 338 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs rename test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/{CompositeValueProviderTests.cs => CompositeValueProviderTest.cs} (80%) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs rename test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/{FormValueProviderFactoryTests.cs => FormValueProviderFactoryTest.cs} (69%) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeValueProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeValueProvider.cs index 78a2b691df..7508513b57 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/CompositeValueProvider.cs @@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// Represents a whose values come from a collection of s. /// public class CompositeValueProvider : - Collection, - IEnumerableValueProvider, + Collection, + IEnumerableValueProvider, IBindingSourceValueProvider { /// @@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// created. /// public static CompositeValueProvider Create( - [NotNull] IEnumerable factories, + [NotNull] IEnumerable factories, [NotNull] ValueProviderFactoryContext context) { var composite = new CompositeValueProvider(); @@ -137,6 +137,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + if (filteredValueProviders.Count == Count) + { + // No need for a new CompositeValueProvider. + return this; + } + return new CompositeValueProvider(filteredValueProviders); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/IBindingSourceValueProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/IBindingSourceValueProvider.cs index 1eb69d687d..752f0fc2e7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/IBindingSourceValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/IBindingSourceValueProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { /// - /// A value provider which is which can filter its contents based on . + /// A value provider which can filter its contents based on . /// /// /// Value providers are by-default included. If a model does not specify a diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs new file mode 100644 index 0000000000..e1e319dd95 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs @@ -0,0 +1,114 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// An for form data stored in an and + /// generally accessed asynchronously. + /// + public class JQueryFormValueProvider : BindingSourceValueProvider, IEnumerableValueProvider + { + private readonly Func>> _valuesFactory; + + private PrefixContainer _prefixContainer; + private IDictionary _values; + + /// + /// Initializes a new instance of the class. + /// + /// The of the data. + /// A delegate which provides the values to wrap. + /// The culture to return with ValueProviderResult instances. + public JQueryFormValueProvider( + [NotNull] BindingSource bindingSource, + [NotNull] Func>> valuesFactory, + CultureInfo culture) + : base(bindingSource) + { + _valuesFactory = valuesFactory; + Culture = culture; + } + + // Internal for testing. + internal JQueryFormValueProvider( + [NotNull] BindingSource bindingSource, + [NotNull] IDictionary values, + CultureInfo culture) + : base(bindingSource) + { + _values = values; + Culture = culture; + } + + // Internal for testing + internal CultureInfo Culture { get; } + + /// + public override async Task ContainsPrefixAsync(string prefix) + { + var prefixContainer = await GetPrefixContainerAsync(); + return prefixContainer.ContainsPrefix(prefix); + } + + /// + public async Task> GetKeysFromPrefixAsync(string prefix) + { + var prefixContainer = await GetPrefixContainerAsync(); + return prefixContainer.GetKeysFromPrefix(prefix); + } + + /// + public override async Task GetValueAsync(string key) + { + var dictionary = await GetDictionary(); + + string[] values; + if (dictionary.TryGetValue(key, out values) && values != null && values.Length > 0) + { + // Success. + if (values.Length == 1) + { + return new ValueProviderResult(values[0], values[0], Culture); + } + + return new ValueProviderResult(values, string.Join(",", values), Culture); + } + + return null; + } + + private async Task> GetDictionary() + { + if (_values == null) + { + Debug.Assert(_valuesFactory != null); + + // Initialization race is OK providing data remains read-only. + _values = await _valuesFactory(); + } + + return _values; + } + + private async Task GetPrefixContainerAsync() + { + if (_prefixContainer == null) + { + var dictionary = await GetDictionary(); + + // Initialization race is OK providing data remains read-only and object identity is not significant. + _prefixContainer = new PrefixContainer(dictionary.Keys); + } + + return _prefixContainer; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs new file mode 100644 index 0000000000..e40cd205c0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs @@ -0,0 +1,113 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class JQueryFormValueProviderFactory : IValueProviderFactory + { + public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context) + { + var request = context.HttpContext.Request; + + if (request.HasFormContentType) + { + return new JQueryFormValueProvider( + BindingSource.Form, + () => GetValueCollectionAsync(request), + CultureInfo.CurrentCulture); + } + + return null; + } + + private static async Task> GetValueCollectionAsync(HttpRequest request) + { + var formCollection = await request.ReadFormAsync(); + + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var entry in formCollection) + { + var key = NormalizeJQueryToMvc(entry.Key); + dictionary[key] = entry.Value; + } + + return dictionary; + } + + // This is a helper method for Model Binding over a JQuery syntax. + // Normalize from JQuery to MVC keys. The model binding infrastructure uses MVC keys. + // x[] --> x + // [] --> "" + // x[12] --> x[12] + // x[field] --> x.field, where field is not a number + private static string NormalizeJQueryToMvc(string key) + { + if (string.IsNullOrEmpty(key)) + { + return string.Empty; + } + + var indexOpen = key.IndexOf('['); + if (indexOpen == -1) + { + + // Fast path, no normalization needed. + // This skips string conversion and allocating the string builder. + return key; + } + + var builder = new StringBuilder(); + var position = 0; + while (position < key.Length) + { + if (indexOpen == -1) + { + // No more brackets. + builder.Append(key, position, key.Length - position); + break; + } + + builder.Append(key, position, indexOpen - position); // everything up to "[" + + // Find closing bracket. + var indexClose = key.IndexOf(']', indexOpen); + if (indexClose == -1) + { + throw new ArgumentException( + message: Resources.FormatJQueryFormValueProviderFactory_MissingClosingBracket(key), + paramName: "key"); + } + + if (indexClose == indexOpen + 1) + { + // Empty brackets signify an array. Just remove. + } + else if (char.IsDigit(key[indexOpen + 1])) + { + // Array index. Leave unchanged. + builder.Append(key, indexOpen, indexClose - indexOpen + 1); + } + else + { + // Field name. Convert to dot notation. + builder.Append('.'); + builder.Append(key, indexOpen + 1, indexClose - indexOpen - 1); + } + + position = indexClose + 1; + indexOpen = key.IndexOf('[', position); + } + + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.Core/MvcCoreMvcOptionsSetup.cs index ae68ccf14c..5d92d989c8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcCoreMvcOptionsSetup.cs @@ -47,6 +47,7 @@ namespace Microsoft.AspNet.Mvc options.ValueProviderFactories.Add(new RouteValueValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); options.ValueProviderFactories.Add(new FormValueProviderFactory()); + options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory()); // Set up metadata providers options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider()); diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index ca9ccc6dd3..ef35c6b217 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1034,6 +1034,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1); } + /// + /// The key '{0}' is invalid JQuery syntax because it is missing a closing bracket. + /// + internal static string JQueryFormValueProviderFactory_MissingClosingBracket + { + get { return GetString("JQueryFormValueProviderFactory_MissingClosingBracket"); } + } + + /// + /// The key '{0}' is invalid JQuery syntax because it is missing a closing bracket. + /// + internal static string FormatJQueryFormValueProviderFactory_MissingClosingBracket(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("JQueryFormValueProviderFactory_MissingClosingBracket"), p0); + } + /// /// A value is required. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index bb5aa0eaa8..7ba416e14c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -318,6 +318,9 @@ The property {0}.{1} could not be found. + + The key '{0}' is invalid JQuery syntax because it is missing a closing bracket. + A value is required. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs similarity index 80% rename from test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTests.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs index 6af057aa15..c341a04f9f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs @@ -2,17 +2,41 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if DNX451 - using System; +#endif using System.Collections.Generic; +using System.Globalization; +#if DNX451 using System.Threading.Tasks; using Moq; using Xunit; +#endif namespace Microsoft.AspNet.Mvc.ModelBinding { - public class CompositeValueProviderTests + public class CompositeValueProviderTest : EnumerableValueProviderTest { + protected override IEnumerableValueProvider GetEnumerableValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) + { + var emptyValueProvider = + new JQueryFormValueProvider(bindingSource, new Dictionary(), culture); + var valueProvider = new JQueryFormValueProvider(bindingSource, values, culture); + + return new CompositeValueProvider(new[] { emptyValueProvider, valueProvider }); + } + + protected override void CheckFilterExcludeResult(IValueProvider result) + { + // CompositeValueProvider returns an empty instance rather than null. CompositeModelBinder and + // MutableObjectModelBinder depend on this empty instance. + var compositeProvider = Assert.IsType(result); + Assert.Empty(compositeProvider); + } + +#if DNX451 [Fact] public async Task GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues() { @@ -125,6 +149,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } } +#endif } } -#endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs new file mode 100644 index 0000000000..6cd11dd52f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs @@ -0,0 +1,302 @@ +// 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. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public abstract class EnumerableValueProviderTest + { + private static readonly IDictionary _backingStore = new Dictionary + { + { "some", new[] { "someValue1", "someValue2" } }, + { "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] + public async Task ContainsPrefixAsync_WithEmptyCollection_ReturnsFalseForEmptyPrefix() + { + // Arrange + var backingStore = new Dictionary(); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, backingStore, culture: null); + + // Act + var result = await valueProvider.ContainsPrefixAsync(string.Empty); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix() + { + // Arrange + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.ContainsPrefixAsync(string.Empty); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes() + { + // Arrange + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act & Assert + 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] + public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix() + { + // Arrange + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.ContainsPrefixAsync("biff"); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task GetKeysFromPrefixAsync_EmptyPrefix_ReturnsAllPrefixes() + { + // Arrange + var expected = new Dictionary + { + { "index", "[index]" }, + { "null_value", "null_value" }, + { "prefix", "prefix" }, + { "some", "some" }, + }; + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.GetKeysFromPrefixAsync(string.Empty); + + // Assert + Assert.Equal(expected, result.OrderBy(kvp => kvp.Key)); + } + + [Fact] + public async Task GetKeysFromPrefixAsync_UnknownPrefix_ReturnsEmptyDictionary() + { + // Arrange + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.GetKeysFromPrefixAsync("abc"); + + // Assert + Assert.Empty(result); + } + + [Fact] + 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 = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.GetKeysFromPrefixAsync("prefix"); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public async Task GetKeysFromPrefixAsync_IndexPrefix_ReturnsMatchingItems() + { + // Arrange + var expected = new Dictionary + { + { "property", "[index].property" }, + { "anotherIndex", "[index][anotherIndex]" } + }; + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.GetKeysFromPrefixAsync("[index]"); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public async Task GetValueAsync_SingleValue() + { + // Arrange + var culture = new CultureInfo("fr-FR"); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + + // Act + var result = await valueProvider.GetValueAsync("prefix.name"); + + // Assert + Assert.NotNull(result); + Assert.Equal("someOtherValue", result.RawValue); + Assert.Equal("someOtherValue", result.AttemptedValue); + Assert.Equal(culture, result.Culture); + } + + [Fact] + public async Task GetValueAsync_MultiValue() + { + // Arrange + var culture = new CultureInfo("fr-FR"); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + + // Act + var result = await valueProvider.GetValueAsync("some"); + + // Assert + Assert.NotNull(result); + Assert.Equal(new[] { "someValue1", "someValue2" }, (IList)result.RawValue); + Assert.Equal("someValue1,someValue2", result.AttemptedValue); + Assert.Equal(culture, result.Culture); + } + + [Theory] + [InlineData("null_value")] + [InlineData("prefix.null_value")] + public async Task GetValue_NullValue(string key) + { + // Arrange + var culture = new CultureInfo("fr-FR"); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture); + + // Act + var result = await valueProvider.GetValueAsync(key); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetValueAsync_NullMultipleValue() + { + // Arrange + var backingStore = new Dictionary + { + { "key", new string[] { null, null, "value" } }, + }; + var culture = new CultureInfo("fr-FR"); + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, backingStore, culture); + + // Act + var result = await valueProvider.GetValueAsync("key"); + + // Assert + Assert.Equal(new[] { null, null, "value" }, result.RawValue as IEnumerable); + Assert.Equal(",,value", result.AttemptedValue); + } + + [Fact] + public async Task GetValueAsync_ReturnsNullIfKeyNotFound() + { + // Arrange + var valueProvider = GetEnumerableValueProvider(BindingSource.Query, _backingStore, culture: null); + + // Act + var result = await valueProvider.GetValueAsync("prefix"); + + // Assert + Assert.Null(result); + } + + [Fact] + public void FilterInclude() + { + // Arrange + var provider = GetBindingSourceValueProvider(BindingSource.Query, _backingStore, culture: null); + + var bindingSource = new BindingSource( + BindingSource.Query.Id, + displayName: null, + isGreedy: true, + isFromRequest: true); + + // Act + var result = provider.Filter(bindingSource); + + // Assert + Assert.NotNull(result); + Assert.Same(result, provider); + } + + [Fact] + public void FilterExclude() + { + // Arrange + var provider = GetBindingSourceValueProvider(BindingSource.Query, _backingStore, culture: null); + + var bindingSource = new BindingSource( + "Test", + displayName: null, + isGreedy: true, + isFromRequest: true); + + // Act + var result = provider.Filter(bindingSource); + + // Assert + CheckFilterExcludeResult(result); + } + + protected virtual void CheckFilterExcludeResult(IValueProvider result) + { + Assert.Null(result); + } + + private IBindingSourceValueProvider GetBindingSourceValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) + { + var provider = GetEnumerableValueProvider(bindingSource, values, culture) as IBindingSourceValueProvider; + + // All IEnumerableValueProvider implementations also implement IBindingSourceValueProvider. + Assert.NotNull(provider); + + return provider; + } + + protected abstract IEnumerableValueProvider GetEnumerableValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture); + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTest.cs similarity index 69% rename from test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTests.cs rename to test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTest.cs index de41590e31..5ef29bcea4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/FormValueProviderFactoryTest.cs @@ -1,20 +1,15 @@ // 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. -#if DNX451 using System; using System.Collections.Generic; using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Features.Internal; -using Moq; +using Microsoft.AspNet.Http.Internal; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Test { - public class FormValueProviderFactoryTests + public class FormValueProviderFactoryTest { [Fact] public void GetValueProvider_ReturnsNull_WhenContentTypeIsNotFormUrlEncoded() @@ -35,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test [InlineData("application/x-www-form-urlencoded;charset=utf-8")] [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq")] [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq; charset=utf-8")] - public void GetValueProvider_ReturnsValueProviderInstanceWithInvariantCulture(string contentType) + public void GetValueProvider_ReturnsValueProviderInstanceWithCurrentCulture(string contentType) { // Arrange var context = CreateContext(contentType); @@ -51,19 +46,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test private static ValueProviderFactoryContext CreateContext(string contentType) { - var collection = Mock.Of(); - var request = new Mock(); - request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection)); - request.SetupGet(r => r.ContentType).Returns(contentType); - request.SetupGet(r => r.HasFormContentType).Returns(new FormFeature(request.Object).HasFormContentType); - - var context = new Mock(); - context.SetupGet(c => c.Request).Returns(request.Object); + var context = new DefaultHttpContext(); + context.Request.ContentType = contentType; return new ValueProviderFactoryContext( - context.Object, + context, new Dictionary(StringComparer.OrdinalIgnoreCase)); } } } -#endif diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs new file mode 100644 index 0000000000..c95b2691d5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs @@ -0,0 +1,135 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Internal; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class JQueryFormValueProviderFactoryTest + { + private static readonly IDictionary _backingStore = new Dictionary + { + { "[]", new[] { "found" } }, + { "[]property1", new[] { "found" } }, + { "property2[]", new[] { "found" } }, + { "[]property3[]", new[] { "found" } }, + { "property[]Value", new[] { "found" } }, + { "[10]", new[] { "found" } }, + { "[11]property", new[] { "found" } }, + { "property4[10]", new[] { "found" } }, + { "[12]property[][13]", new[] { "found" } }, + { "[14][]property1[15]property2", new[] { "found" } }, + { "prefix[11]property1", new[] { "found" } }, + { "prefix[12][][property2]", new[] { "found" } }, + { "prefix[property1][13]", new[] { "found" } }, + { "prefix[14][][15]", new[] { "found" } }, + { "[property5][]", new[] { "found" } }, + { "[][property6]Value", new[] { "found" } }, + { "prefix[property2]", new[] { "found" } }, + { "prefix[][property]Value", new[] { "found" } }, + { "[property7][property8]", new[] { "found" } }, + { "[property9][][property10]Value", new[] { "found" } }, + }; + + [Fact] + public void GetValueProvider_ReturnsNull_WhenContentTypeIsNotFormUrlEncoded() + { + // Arrange + var context = CreateContext("some-content-type", formValues: null); + var factory = new JQueryFormValueProviderFactory(); + + // Act + var result = factory.GetValueProvider(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("application/x-www-form-urlencoded")] + [InlineData("application/x-www-form-urlencoded;charset=utf-8")] + [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq")] + [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq; charset=utf-8")] + public void GetValueProvider_ReturnsExpectedValueProviderInstanceWithCurrentCulture(string contentType) + { + // Arrange + var context = CreateContext(contentType, formValues: null); + var factory = new JQueryFormValueProviderFactory(); + + // Act + var result = factory.GetValueProvider(context); + + // Assert + var valueProvider = Assert.IsType(result); + Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture); + } + + public static TheoryData SuccessDataSet + { + get + { + return new TheoryData + { + string.Empty, + "property1", + "property2", + "property3", + "propertyValue", + "[10]", + "[11]property", + "property4[10]", + "[12]property[13]", + "[14]property1[15]property2", + "prefix.property1[13]", + "prefix[14][15]", + ".property5", + ".property6Value", + "prefix.property2", + "prefix.propertyValue", + ".property7.property8", + ".property9.property10Value", + }; + } + } + + [Theory] + [MemberData(nameof(SuccessDataSet))] + public async Task GetValueProvider_ReturnsValueProvider_ContainingExpectedKeys(string key) + { + // Arrange + var context = CreateContext("application/x-www-form-urlencoded", formValues: _backingStore); + var factory = new JQueryFormValueProviderFactory(); + + // Act + var valueProvider = factory.GetValueProvider(context); + var result = await valueProvider.GetValueAsync(key); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.RawValue); + var value = Assert.IsType(result.RawValue); + Assert.Equal("found", value); + } + + private static ValueProviderFactoryContext CreateContext( + string contentType, + IDictionary formValues) + { + var context = new DefaultHttpContext(); + context.Request.ContentType = contentType; + if (formValues != null) + { + context.Request.Form = new FormCollection(formValues); + } + + return new ValueProviderFactoryContext( + context, + new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs new file mode 100644 index 0000000000..4fa1b91d66 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderTest.cs @@ -0,0 +1,19 @@ +// 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. + +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class JQueryFormValueProviderTest : EnumerableValueProviderTest + { + protected override IEnumerableValueProvider GetEnumerableValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) + { + return new JQueryFormValueProvider(bindingSource, values, culture); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs index a8d51dc2ad..9a3cd94490 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ReadableStringCollectionValueProviderTest.cs @@ -1,283 +1,21 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; -using Xunit; -namespace Microsoft.AspNet.Mvc.ModelBinding.Test +namespace Microsoft.AspNet.Mvc.ModelBinding { - public class ReadableStringCollectionValueProviderTest + public class ReadableStringCollectionValueProviderTest : EnumerableValueProviderTest { - private static readonly IReadableStringCollection _backingStore = new ReadableStringCollection( - new Dictionary - { - { "some", new[] { "someValue1", "someValue2" } }, - { "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] - public async Task ContainsPrefixAsync_WithEmptyCollection_ReturnsFalseForEmptyPrefix() + protected override IEnumerableValueProvider GetEnumerableValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) { - // Arrange - var backingStore = new ReadableStringCollection(new Dictionary()); - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, null); - - // Act - var result = await valueProvider.ContainsPrefixAsync(string.Empty); - - // Assert - Assert.False(result); - } - - [Fact] - public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix() - { - // Arrange - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - // Act - var result = await valueProvider.ContainsPrefixAsync(string.Empty); - - // Assert - Assert.True(result); - } - - [Fact] - public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes() - { - // Arrange - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - // Act & Assert - 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] - public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix() - { - // Arrange - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - // Act - var result = await valueProvider.ContainsPrefixAsync("biff"); - - // Assert - Assert.False(result); - } - - [Fact] - public async Task GetKeysFromPrefixAsync_EmptyPrefix_ReturnsAllPrefixes() - { - // Arrange - var expected = new Dictionary - { - { "index", "[index]" }, - { "null_value", "null_value" }, - { "prefix", "prefix" }, - { "some", "some" }, - }; - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture: null); - - // Act - var result = await valueProvider.GetKeysFromPrefixAsync(string.Empty); - - // Assert - Assert.Equal(expected, result.OrderBy(kvp => kvp.Key)); - } - - [Fact] - public async Task GetKeysFromPrefixAsync_UnknownPrefix_ReturnsEmptyDictionary() - { - // Arrange - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - // Act - var result = await valueProvider.GetKeysFromPrefixAsync("abc"); - - // Assert - Assert.Empty(result); - } - - [Fact] - 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("prefix"); - - // Assert - 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] - public async Task GetValueAsync_SingleValue() - { - // Arrange - var culture = new CultureInfo("fr-FR"); - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture); - - // Act - var result = await valueProvider.GetValueAsync("prefix.name"); - - // Assert - Assert.NotNull(result); - Assert.Equal("someOtherValue", result.RawValue); - Assert.Equal("someOtherValue", result.AttemptedValue); - Assert.Equal(culture, result.Culture); - } - - [Fact] - public async Task GetValueAsync_MultiValue() - { - // Arrange - var culture = new CultureInfo("fr-FR"); - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture); - - // Act - var result = await valueProvider.GetValueAsync("some"); - - // Assert - Assert.NotNull(result); - Assert.Equal(new[] { "someValue1", "someValue2" }, (IList)result.RawValue); - Assert.Equal("someValue1,someValue2", result.AttemptedValue); - Assert.Equal(culture, result.Culture); - } - - [Theory] - [InlineData("null_value")] - [InlineData("prefix.null_value")] - public async Task GetValue_NullValue(string key) - { - // Arrange - var culture = new CultureInfo("fr-FR"); - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture); - - // Act - var result = await valueProvider.GetValueAsync(key); - - // Assert - Assert.Null(result); - } - - [Fact] - public async Task GetValueAsync_NullMultipleValue() - { - // Arrange - var backingStore = new ReadableStringCollection( - new Dictionary - { - { "key", new string[] { null, null, "value" } } - }); - var culture = new CultureInfo("fr-FR"); - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, culture); - - // Act - var result = await valueProvider.GetValueAsync("key"); - - // Assert - Assert.Equal(new[] { null, null, "value" }, result.RawValue as IEnumerable); - Assert.Equal(",,value", result.AttemptedValue); - } - - [Fact] - public async Task GetValueAsync_ReturnsNullIfKeyNotFound() - { - // Arrange - var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - // Act - var result = await valueProvider.GetValueAsync("prefix"); - - // Assert - Assert.Null(result); - } - - [Fact] - public void FilterInclude() - { - // Arrange - var provider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - var bindingSource = new BindingSource( - BindingSource.Query.Id, - displayName: null, - isGreedy: true, - isFromRequest: true); - - // Act - var result = provider.Filter(bindingSource); - - // Assert - Assert.NotNull(result); - Assert.Same(result, provider); - } - - [Fact] - public void FilterExclude() - { - // Arrange - var provider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null); - - var bindingSource = new BindingSource( - "Test", - displayName: null, - isGreedy: true, - isFromRequest: true); - - // Act - var result = provider.Filter(bindingSource); - - // Assert - Assert.Null(result); + var backingStore = new ReadableStringCollection(values); + return new ReadableStringCollectionValueProvider(bindingSource, backingStore, culture); } } } diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/TypeConverterModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/TypeConverterModelBinderIntegrationTest.cs index ecca5e01ba..8170475234 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/TypeConverterModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/TypeConverterModelBinderIntegrationTest.cs @@ -3,8 +3,11 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.ModelBinding; using Xunit; @@ -12,16 +15,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests { public class TypeConverterModelBinderIntegrationTest { - private class Person - { - public Address Address { get; set; } - } - - private class Address - { - public int Zip { get; set; } - } - [Fact] public async Task BindProperty_WithData_WithPrefix_GetsBound() { @@ -266,9 +259,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests }; // No Data. - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => - { - }); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); var modelState = new ModelStateDictionary(); @@ -284,5 +275,90 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Assert.True(modelState.IsValid); Assert.Empty(modelState.Keys); } + + public static TheoryData> PersonStoreData + { + get + { + return new TheoryData> + { + new Dictionary + { + { "name", new[] { "Fred" } }, + { "address.zip", new[] { "98052" } }, + { "address.lines", new[] { "line 1", "line 2" } }, + }, + new Dictionary + { + { "address.lines[]", new[] { "line 1", "line 2" } }, + { "address[].zip", new[] { "98052" } }, + { "name[]", new[] { "Fred" } }, + } + }; + } + } + + [Theory] + [MemberData(nameof(PersonStoreData))] + public async Task BindParameter_FromFormData_BindsCorrectly(IDictionary personStore) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person), + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + request.Form = new FormCollection(personStore); + }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.Equal("Fred", boundPerson.Name); + Assert.NotNull(boundPerson.Address); + Assert.Equal(new[] { "line 1", "line 2" }, boundPerson.Address.Lines); + Assert.Equal(98052, boundPerson.Address.Zip); + + // ModelState + Assert.True(modelState.IsValid); + + Assert.Equal(new[] { "Address.Lines", "Address.Zip", "Name" }, modelState.Keys.ToArray()); + var entry = modelState["Address.Lines"]; + Assert.NotNull(entry); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + var result = entry.Value; + Assert.NotNull(result); + Assert.Equal("line 1,line 2", result.AttemptedValue); + Assert.Equal(new[] { "line 1", "line 2" }, result.RawValue); + } + + private class Person + { + public Address Address { get; set; } + + public string Name { get; set; } + } + + private class Address + { + public string[] Lines { get; set; } + + public int Zip { get; set; } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs index 76e27ec778..7f8e2b6aaa 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs @@ -62,10 +62,11 @@ namespace Microsoft.AspNet.Mvc // Assert var valueProviders = options.ValueProviderFactories; - Assert.Equal(3, valueProviders.Count); + Assert.Equal(4, valueProviders.Count); Assert.IsType(valueProviders[0]); Assert.IsType(valueProviders[1]); Assert.IsType(valueProviders[2]); + Assert.IsType(valueProviders[3]); } [Fact] @@ -174,7 +175,7 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(xObjectFilter.ExcludedTypeName, typeof(XObject).FullName); Assert.IsType(typeof(DefaultTypeNameBasedExcludeFilter), options.ValidationExcludeFilters[i]); - var xmlNodeFilter = + var xmlNodeFilter = Assert.IsType(options.ValidationExcludeFilters[i++]); Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode"); }