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`
This commit is contained in:
Doug Bunting 2015-08-03 08:00:03 -07:00
parent a7d717d19c
commit bda850187d
15 changed files with 874 additions and 338 deletions

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Represents a <see cref="IValueProvider"/> whose values come from a collection of <see cref="IValueProvider"/>s.
/// </summary>
public class CompositeValueProvider :
Collection<IValueProvider>,
IEnumerableValueProvider,
Collection<IValueProvider>,
IEnumerableValueProvider,
IBindingSourceValueProvider
{
/// <summary>
@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// created.
/// </returns>
public static CompositeValueProvider Create(
[NotNull] IEnumerable<IValueProviderFactory> factories,
[NotNull] IEnumerable<IValueProviderFactory> 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);
}
}

View File

@ -6,7 +6,7 @@ using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A value provider which is which can filter its contents based on <see cref="BindingSource"/>.
/// A value provider which can filter its contents based on <see cref="BindingSource"/>.
/// </summary>
/// <remarks>
/// Value providers are by-default included. If a model does not specify a <see cref="BindingSource"/>

View File

@ -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
{
/// <summary>
/// An <see cref="IValueProvider"/> for form data stored in an <see cref="IDictionary{string, string[]}"/> and
/// generally accessed asynchronously.
/// </summary>
public class JQueryFormValueProvider : BindingSourceValueProvider, IEnumerableValueProvider
{
private readonly Func<Task<IDictionary<string, string[]>>> _valuesFactory;
private PrefixContainer _prefixContainer;
private IDictionary<string, string[]> _values;
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryBasedValueProvider"/> class.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> of the data.</param>
/// <param name="valuesFactory">A delegate which provides the values to wrap.</param>
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
public JQueryFormValueProvider(
[NotNull] BindingSource bindingSource,
[NotNull] Func<Task<IDictionary<string, string[]>>> valuesFactory,
CultureInfo culture)
: base(bindingSource)
{
_valuesFactory = valuesFactory;
Culture = culture;
}
// Internal for testing.
internal JQueryFormValueProvider(
[NotNull] BindingSource bindingSource,
[NotNull] IDictionary<string, string[]> values,
CultureInfo culture)
: base(bindingSource)
{
_values = values;
Culture = culture;
}
// Internal for testing
internal CultureInfo Culture { get; }
/// <inheritdoc />
public override async Task<bool> ContainsPrefixAsync(string prefix)
{
var prefixContainer = await GetPrefixContainerAsync();
return prefixContainer.ContainsPrefix(prefix);
}
/// <inheritdoc />
public async Task<IDictionary<string, string>> GetKeysFromPrefixAsync(string prefix)
{
var prefixContainer = await GetPrefixContainerAsync();
return prefixContainer.GetKeysFromPrefix(prefix);
}
/// <inheritdoc />
public override async Task<ValueProviderResult> 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<IDictionary<string, string[]>> 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<PrefixContainer> 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;
}
}
}

View File

@ -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<IDictionary<string, string[]>> GetValueCollectionAsync(HttpRequest request)
{
var formCollection = await request.ReadFormAsync();
var dictionary = new Dictionary<string, string[]>(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();
}
}
}

View File

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

View File

@ -1034,6 +1034,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PropertyNotFound"), p0, p1);
}
/// <summary>
/// The key '{0}' is invalid JQuery syntax because it is missing a closing bracket.
/// </summary>
internal static string JQueryFormValueProviderFactory_MissingClosingBracket
{
get { return GetString("JQueryFormValueProviderFactory_MissingClosingBracket"); }
}
/// <summary>
/// The key '{0}' is invalid JQuery syntax because it is missing a closing bracket.
/// </summary>
internal static string FormatJQueryFormValueProviderFactory_MissingClosingBracket(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("JQueryFormValueProviderFactory_MissingClosingBracket"), p0);
}
/// <summary>
/// A value is required.
/// </summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -318,6 +318,9 @@
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
<data name="JQueryFormValueProviderFactory_MissingClosingBracket" xml:space="preserve">
<value>The key '{0}' is invalid JQuery syntax because it is missing a closing bracket.</value>
</data>
<data name="KeyValuePair_BothKeyAndValueMustBePresent" xml:space="preserve">
<value>A value is required.</value>
</data>

View File

@ -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<string, string[]> values,
CultureInfo culture)
{
var emptyValueProvider =
new JQueryFormValueProvider(bindingSource, new Dictionary<string, string[]>(), 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<CompositeValueProvider>(result);
Assert.Empty(compositeProvider);
}
#if DNX451
[Fact]
public async Task GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues()
{
@ -125,6 +149,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
}
#endif
}
}
#endif

View File

@ -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<string, string[]> _backingStore = new Dictionary<string, string[]>
{
{ "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<string, string[]>();
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<string, string>
{
{ "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<string, string>
{
{ "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<string, string>
{
{ "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<string>)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<string, string[]>
{
{ "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<string>);
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<string, string[]> 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<string, string[]> values,
CultureInfo culture);
}
}

View File

@ -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<IFormCollection>();
var request = new Mock<HttpRequest>();
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<HttpContext>();
context.SetupGet(c => c.Request).Returns(request.Object);
var context = new DefaultHttpContext();
context.Request.ContentType = contentType;
return new ValueProviderFactoryContext(
context.Object,
context,
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase));
}
}
}
#endif

View File

@ -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<string, string[]> _backingStore = new Dictionary<string, string[]>
{
{ "[]", 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<JQueryFormValueProvider>(result);
Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture);
}
public static TheoryData<string> SuccessDataSet
{
get
{
return new TheoryData<string>
{
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<string>(result.RawValue);
Assert.Equal("found", value);
}
private static ValueProviderFactoryContext CreateContext(
string contentType,
IDictionary<string, string[]> formValues)
{
var context = new DefaultHttpContext();
context.Request.ContentType = contentType;
if (formValues != null)
{
context.Request.Form = new FormCollection(formValues);
}
return new ValueProviderFactoryContext(
context,
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase));
}
}
}

View File

@ -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<string, string[]> values,
CultureInfo culture)
{
return new JQueryFormValueProvider(bindingSource, values, culture);
}
}
}

View File

@ -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<string, string[]>
{
{ "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<string, string[]> values,
CultureInfo culture)
{
// Arrange
var backingStore = new ReadableStringCollection(new Dictionary<string, string[]>());
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<string, string>
{
{ "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<string, string>
{
{ "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<string, string>
{
{ "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<string>)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<string, string[]>
{
{ "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<string>);
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);
}
}
}

View File

@ -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<IDictionary<string, string[]>> PersonStoreData
{
get
{
return new TheoryData<IDictionary<string, string[]>>
{
new Dictionary<string, string[]>
{
{ "name", new[] { "Fred" } },
{ "address.zip", new[] { "98052" } },
{ "address.lines", new[] { "line 1", "line 2" } },
},
new Dictionary<string, string[]>
{
{ "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<string, string[]> 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<Person>(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; }
}
}
}

View File

@ -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<RouteValueValueProviderFactory>(valueProviders[0]);
Assert.IsType<QueryStringValueProviderFactory>(valueProviders[1]);
Assert.IsType<FormValueProviderFactory>(valueProviders[2]);
Assert.IsType<JQueryFormValueProviderFactory>(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<DefaultTypeNameBasedExcludeFilter>(options.ValidationExcludeFilters[i++]);
Assert.Equal(xmlNodeFilter.ExcludedTypeName, "System.Xml.XmlNode");
}