diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs index b9eb3862d0..db1706d964 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProvider.cs @@ -1,10 +1,8 @@ // 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 Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -12,11 +10,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// An for jQuery formatted form data. /// - public class JQueryFormValueProvider : BindingSourceValueProvider, IEnumerableValueProvider + public class JQueryFormValueProvider : JQueryValueProvider { - private readonly IDictionary _values; - private PrefixContainer _prefixContainer; - /// /// Initializes a new instance of the class. /// @@ -27,60 +22,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding BindingSource bindingSource, IDictionary values, CultureInfo culture) - : base(bindingSource) + : base(bindingSource, values, culture) { - if (bindingSource == null) - { - throw new ArgumentNullException(nameof(bindingSource)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - _values = values; - Culture = culture; - } - - // Internal for testing - internal CultureInfo Culture { get; } - - protected PrefixContainer PrefixContainer - { - get - { - if (_prefixContainer == null) - { - _prefixContainer = new PrefixContainer(_values.Keys); - } - - return _prefixContainer; - } - } - - /// - public override bool ContainsPrefix(string prefix) - { - return PrefixContainer.ContainsPrefix(prefix); - } - - /// - public IDictionary GetKeysFromPrefix(string prefix) - { - return PrefixContainer.GetKeysFromPrefix(prefix); - } - - /// - public override ValueProviderResult GetValue(string key) - { - StringValues values; - if (_values.TryGetValue(key, out values) && values.Count > 0) - { - return new ValueProviderResult(values, Culture); - } - - return ValueProviderResult.None; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs index e007ea3160..fdd8eb2d93 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryFormValueProviderFactory.cs @@ -2,18 +2,13 @@ // 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.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Core; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ModelBinding { /// - /// A for . + /// An for . /// public class JQueryFormValueProviderFactory : IValueProviderFactory { @@ -38,97 +33,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) { var request = context.ActionContext.HttpContext.Request; + + var formCollection = await request.ReadFormAsync(); + var valueProvider = new JQueryFormValueProvider( BindingSource.Form, - await GetValueCollectionAsync(request), + JQueryKeyValuePairNormalizer.GetValues(formCollection, formCollection.Count), CultureInfo.CurrentCulture); context.ValueProviders.Add(valueProvider); } - - private static async Task> GetValueCollectionAsync(HttpRequest request) - { - var formCollection = await request.ReadFormAsync(); - - var builder = new StringBuilder(); - var dictionary = new Dictionary( - formCollection.Count, - StringComparer.OrdinalIgnoreCase); - foreach (var entry in formCollection) - { - var key = NormalizeJQueryToMvc(builder, entry.Key); - builder.Clear(); - - 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(StringBuilder builder, 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 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: nameof(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.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs new file mode 100644 index 0000000000..b9fcc2520f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryKeyValuePairNormalizer.cs @@ -0,0 +1,100 @@ +// 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.Text; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + // Normalizes keys, in a keyvaluepair collection, from jQuery format to a format that MVC understands. + internal static class JQueryKeyValuePairNormalizer + { + public static IDictionary GetValues( + IEnumerable> originalValues, + int valueCount) + { + var builder = new StringBuilder(); + var dictionary = new Dictionary( + valueCount, + StringComparer.OrdinalIgnoreCase); + foreach (var originalValue in originalValues) + { + var normalizedKey = NormalizeJQueryToMvc(builder, originalValue.Key); + builder.Clear(); + + dictionary[normalizedKey] = originalValue.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(StringBuilder builder, 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 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: nameof(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.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProvider.cs new file mode 100644 index 0000000000..4d50ab8b88 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProvider.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// An for jQuery formatted query string data. + /// + public class JQueryQueryStringValueProvider : JQueryValueProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The of the data. + /// The values. + /// The culture to return with ValueProviderResult instances. + public JQueryQueryStringValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) + : base(bindingSource, values, culture) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProviderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProviderFactory.cs new file mode 100644 index 0000000000..f454bb6216 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryQueryStringValueProviderFactory.cs @@ -0,0 +1,37 @@ +// 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.Globalization; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// An for . + /// + public class JQueryQueryStringValueProviderFactory : IValueProviderFactory + { + /// + public Task CreateValueProviderAsync(ValueProviderFactoryContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var query = context.ActionContext.HttpContext.Request.Query; + if (query != null && query.Count > 0) + { + var valueProvider = new JQueryQueryStringValueProvider( + BindingSource.Query, + JQueryKeyValuePairNormalizer.GetValues(query, query.Count), + CultureInfo.InvariantCulture); + + context.ValueProviders.Add(valueProvider); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs new file mode 100644 index 0000000000..a2ffca0f2a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/JQueryValueProvider.cs @@ -0,0 +1,89 @@ +// 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 Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// An for jQuery formatted form data. + /// + public abstract class JQueryValueProvider : BindingSourceValueProvider, IEnumerableValueProvider + { + private readonly IDictionary _values; + private PrefixContainer _prefixContainer; + + /// + /// Initializes a new instance of the class. + /// + /// The of the data. + /// The values. + /// The culture to return with ValueProviderResult instances. + protected JQueryValueProvider( + BindingSource bindingSource, + IDictionary values, + CultureInfo culture) + : base(bindingSource) + { + if (bindingSource == null) + { + throw new ArgumentNullException(nameof(bindingSource)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + _values = values; + Culture = culture; + } + + /// + /// Gets the associated with the values. + /// + public CultureInfo Culture { get; } + + /// + protected PrefixContainer PrefixContainer + { + get + { + if (_prefixContainer == null) + { + _prefixContainer = new PrefixContainer(_values.Keys); + } + + return _prefixContainer; + } + } + + /// + public override bool ContainsPrefix(string prefix) + { + return PrefixContainer.ContainsPrefix(prefix); + } + + /// + public IDictionary GetKeysFromPrefix(string prefix) + { + return PrefixContainer.GetKeysFromPrefix(prefix); + } + + /// + public override ValueProviderResult GetValue(string key) + { + StringValues values; + if (_values.TryGetValue(key, out values) && values.Count > 0) + { + return new ValueProviderResult(values, Culture); + } + + return ValueProviderResult.None; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/QueryStringValueProviderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/QueryStringValueProviderFactory.cs index 435963bee5..b6e620df82 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/QueryStringValueProviderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/QueryStringValueProviderFactory.cs @@ -21,12 +21,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(context)); } - var valueProvider = new QueryStringValueProvider( - BindingSource.Query, - context.ActionContext.HttpContext.Request.Query, - CultureInfo.InvariantCulture); + var query = context.ActionContext.HttpContext.Request.Query; + if (query != null && query.Count > 0) + { + var valueProvider = new QueryStringValueProvider( + BindingSource.Query, + query, + CultureInfo.InvariantCulture); - context.ValueProviders.Add(valueProvider); + context.ValueProviders.Add(valueProvider); + } return Task.CompletedTask; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json index 07cf4ab0af..9546877aea 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json +++ b/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json @@ -28,5 +28,9 @@ "TypeId": "public class Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinderProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderProvider", "MemberId": "public .ctor(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Internal.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.MvcOptions options)", "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Mvc.ModelBinding.JQueryFormValueProvider : Microsoft.AspNetCore.Mvc.ModelBinding.BindingSourceValueProvider, Microsoft.AspNetCore.Mvc.ModelBinding.IEnumerableValueProvider", + "Kind": "Removal" } ] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs index 92049bfa83..b968a92f19 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryFormValueProviderFactoryTest.cs @@ -116,6 +116,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test Assert.Equal("found", (string)result); } + [Fact] + public async Task CreatesValueProvider_WithCurrentCulture() + { + // Arrange + var context = CreateContext("application/x-www-form-urlencoded", formValues: _backingStore); + var factory = new JQueryFormValueProviderFactory(); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + var valueProvider = Assert.Single(context.ValueProviders); + var jqueryFormValueProvider = Assert.IsType(valueProvider); + Assert.Equal(CultureInfo.CurrentCulture, jqueryFormValueProvider.Culture); + } + private static ValueProviderFactoryContext CreateContext(string contentType, Dictionary formValues) { var context = new DefaultHttpContext(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs new file mode 100644 index 0000000000..a3133261c3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderFactoryTest.cs @@ -0,0 +1,127 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test +{ + public class JQueryQueryStringValueProviderFactoryTest + { + private static readonly Dictionary _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" } }, + }; + + 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(_backingStore); + var factory = new JQueryQueryStringValueProviderFactory(); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + var valueProvider = Assert.Single(context.ValueProviders); + var result = valueProvider.GetValue(key); + Assert.Equal("found", (string)result); + } + + [Fact] + public async Task DoesNotCreateValueProvider_WhenQueryIsEmpty() + { + // Arrange + var context = CreateContext(new Dictionary()); + var factory = new JQueryQueryStringValueProviderFactory(); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + Assert.Empty(context.ValueProviders); + } + + [Fact] + public async Task CreatesValueProvider_WithInvariantCulture() + { + // Arrange + var context = CreateContext(_backingStore); + var factory = new JQueryQueryStringValueProviderFactory(); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + var valueProvider = Assert.Single(context.ValueProviders); + var jqueryQueryStringValueProvider = Assert.IsType(valueProvider); + Assert.Equal(CultureInfo.InvariantCulture, jqueryQueryStringValueProvider.Culture); + } + + private static ValueProviderFactoryContext CreateContext(Dictionary queryStringValues) + { + var context = new DefaultHttpContext(); + + context.Request.Query = new QueryCollection(queryStringValues); + + var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor()); + return new ValueProviderFactoryContext(actionContext); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs new file mode 100644 index 0000000000..cb8453fd6b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/JQueryQueryStringValueProviderTest.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + public class JQueryQueryStringValueProviderTest : EnumerableValueProviderTest + { + protected override IEnumerableValueProvider GetEnumerableValueProvider( + BindingSource bindingSource, + Dictionary values, + CultureInfo culture) + { + return new JQueryQueryStringValueProvider(bindingSource, values, culture); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/QueryStringValueProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/QueryStringValueProviderFactoryTest.cs index 092bc5c08d..40dbfb0d3c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/QueryStringValueProviderFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/QueryStringValueProviderFactoryTest.cs @@ -5,25 +5,40 @@ using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Routing; -using Moq; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test { public class QueryStringValueProviderFactoryTest { + [Fact] + public async Task DoesNotCreateValueProvider_WhenQueryStringIsEmpty() + { + // Arrange + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var factoryContext = new ValueProviderFactoryContext(actionContext); + var factory = new QueryStringValueProviderFactory(); + + // Act + await factory.CreateValueProviderAsync(factoryContext); + + // Assert + Assert.Empty(factoryContext.ValueProviders); + } + [Fact] public async Task GetValueProvider_ReturnsQueryStringValueProviderInstanceWithInvariantCulture() { // Arrange - var request = new Mock(); - request.SetupGet(f => f.Query).Returns(Mock.Of()); - var context = new Mock(); - context.SetupGet(c => c.Items).Returns(new Dictionary()); - context.SetupGet(c => c.Request).Returns(request.Object); - var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor()); + var queryValues = new Dictionary(); + queryValues.Add("foo", "bar"); + var context = new DefaultHttpContext(); + context.Request.Query = new QueryCollection(queryValues); + var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor()); var factoryContext = new ValueProviderFactoryContext(actionContext); var factory = new QueryStringValueProviderFactory(); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/JQueryFormatModelBindingIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/JQueryFormatModelBindingIntegrationTest.cs new file mode 100644 index 0000000000..9376c85e92 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/JQueryFormatModelBindingIntegrationTest.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.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.IntegrationTests +{ + public class JQueryFormatModelBindingIntegrationTest + { + [Fact] + public async Task BindsJQueryFormatData_FromQuery() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Customer) + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = new QueryString( + "?Name=James&Address[0][City]=Redmond&Address[0][State][ShortName]=WA&Address[0][State][LongName]=Washington"); + }, + options => + { + options.ValueProviderFactories.Add(new JQueryQueryStringValueProviderFactory()); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("James", model.Name); + Assert.NotNull(model.Address); + var address = Assert.Single(model.Address); + Assert.Equal("Redmond", address.City); + Assert.NotNull(address.State); + Assert.Equal("WA", address.State.ShortName); + Assert.Equal("Washington", address.State.LongName); + Assert.True(modelState.IsValid); + } + + [Fact] + public async Task BindsJQueryFormatData_FromRequestBody() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "parameter", + ParameterType = typeof(Customer) + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes( + "Name=James&Address[0][City]=Redmond&Address[0][State][ShortName]=WA&Address[0][State][LongName]=Washington")); + request.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("James", model.Name); + Assert.NotNull(model.Address); + var address = Assert.Single(model.Address); + Assert.Equal("Redmond", address.City); + Assert.NotNull(address.State); + Assert.Equal("WA", address.State.ShortName); + Assert.Equal("Washington", address.State.LongName); + Assert.True(modelState.IsValid); + } + + private class Customer + { + public string Name { get; set; } + public List
Address { get; set; } + } + + private class Address + { + public string City { get; set; } + public State State { get; set; } + } + + private class State + { + public string ShortName { get; set; } + public string LongName { get; set; } + } + } +} \ No newline at end of file