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