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

@ -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); return new CompositeValueProvider(filteredValueProviders);
} }
} }

View File

@ -6,7 +6,7 @@ using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
/// <summary> /// <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> /// </summary>
/// <remarks> /// <remarks>
/// Value providers are by-default included. If a model does not specify a <see cref="BindingSource"/> /// 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 RouteValueValueProviderFactory());
options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory());
options.ValueProviderFactories.Add(new FormValueProviderFactory()); options.ValueProviderFactories.Add(new FormValueProviderFactory());
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());
// Set up metadata providers // Set up metadata providers
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider()); 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); 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> /// <summary>
/// A value is required. /// A value is required.
/// </summary> /// </summary>

View File

@ -318,6 +318,9 @@
<data name="Common_PropertyNotFound" xml:space="preserve"> <data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value> <value>The property {0}.{1} could not be found.</value>
</data> </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"> <data name="KeyValuePair_BothKeyAndValueMustBePresent" xml:space="preserve">
<value>A value is required.</value> <value>A value is required.</value>
</data> </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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DNX451 #if DNX451
using System; using System;
#endif
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
#if DNX451
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq; using Moq;
using Xunit; using Xunit;
#endif
namespace Microsoft.AspNet.Mvc.ModelBinding 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] [Fact]
public async Task GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues() 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. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DNX451
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Threading; using Microsoft.AspNet.Http.Internal;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features.Internal;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ {
public class FormValueProviderFactoryTests public class FormValueProviderFactoryTest
{ {
[Fact] [Fact]
public void GetValueProvider_ReturnsNull_WhenContentTypeIsNotFormUrlEncoded() 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("application/x-www-form-urlencoded;charset=utf-8")]
[InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq")] [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq")]
[InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq; charset=utf-8")] [InlineData("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq; charset=utf-8")]
public void GetValueProvider_ReturnsValueProviderInstanceWithInvariantCulture(string contentType) public void GetValueProvider_ReturnsValueProviderInstanceWithCurrentCulture(string contentType)
{ {
// Arrange // Arrange
var context = CreateContext(contentType); var context = CreateContext(contentType);
@ -51,19 +46,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
private static ValueProviderFactoryContext CreateContext(string contentType) private static ValueProviderFactoryContext CreateContext(string contentType)
{ {
var collection = Mock.Of<IFormCollection>(); var context = new DefaultHttpContext();
var request = new Mock<HttpRequest>(); context.Request.ContentType = contentType;
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);
return new ValueProviderFactoryContext( return new ValueProviderFactoryContext(
context.Object, context,
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal; 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( protected override IEnumerableValueProvider GetEnumerableValueProvider(
new Dictionary<string, string[]> BindingSource bindingSource,
{ IDictionary<string, string[]> values,
{ "some", new[] { "someValue1", "someValue2" } }, CultureInfo culture)
{ "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 ReadableStringCollection(values);
var backingStore = new ReadableStringCollection(new Dictionary<string, string[]>()); return new ReadableStringCollectionValueProvider(bindingSource, backingStore, culture);
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);
} }
} }
} }

View File

@ -3,8 +3,11 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit; using Xunit;
@ -12,16 +15,6 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
{ {
public class TypeConverterModelBinderIntegrationTest public class TypeConverterModelBinderIntegrationTest
{ {
private class Person
{
public Address Address { get; set; }
}
private class Address
{
public int Zip { get; set; }
}
[Fact] [Fact]
public async Task BindProperty_WithData_WithPrefix_GetsBound() public async Task BindProperty_WithData_WithPrefix_GetsBound()
{ {
@ -266,9 +259,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
}; };
// No Data. // No Data.
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => var operationContext = ModelBindingTestHelper.GetOperationBindingContext();
{
});
var modelState = new ModelStateDictionary(); var modelState = new ModelStateDictionary();
@ -284,5 +275,90 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
Assert.Empty(modelState.Keys); 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 // Assert
var valueProviders = options.ValueProviderFactories; var valueProviders = options.ValueProviderFactories;
Assert.Equal(3, valueProviders.Count); Assert.Equal(4, valueProviders.Count);
Assert.IsType<RouteValueValueProviderFactory>(valueProviders[0]); Assert.IsType<RouteValueValueProviderFactory>(valueProviders[0]);
Assert.IsType<QueryStringValueProviderFactory>(valueProviders[1]); Assert.IsType<QueryStringValueProviderFactory>(valueProviders[1]);
Assert.IsType<FormValueProviderFactory>(valueProviders[2]); Assert.IsType<FormValueProviderFactory>(valueProviders[2]);
Assert.IsType<JQueryFormValueProviderFactory>(valueProviders[3]);
} }
[Fact] [Fact]