Merge branch 'release/2.1' into dev

This commit is contained in:
Doug Bunting 2018-03-19 13:08:36 -07:00
commit 1aeaf69f87
No known key found for this signature in database
GPG Key ID: 888B4EB7822B32E9
12 changed files with 507 additions and 40 deletions

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc
/// A type that wraps either an <typeparamref name="TValue"/> instance or an <see cref="ActionResult"/>.
/// </summary>
/// <typeparam name="TValue">The type of the result.</typeparam>
public class ActionResult<TValue> : IConvertToActionResult
public sealed class ActionResult<TValue> : IConvertToActionResult
{
/// <summary>
/// Initializes a new instance of <see cref="ActionResult{TValue}"/> using the specified <paramref name="value"/>.

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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&parameter[1].Id=11")]
[InlineData("?parameter.index=low&parameter.index=high&parameter[low].Id=10&parameter[high].Id=11")]
[InlineData("?parameter.index=index&parameter.index=indexer&parameter[index].Id=10&parameter[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&parameter[low].Street=LongStreet")]
[InlineData("?parameter.index=index&parameter[index].Street=LongStreet")]
public async Task CollectionModelBinder_BindsCollectionOfComplexType_ImpliedPrefix_FindsValidationErrors(
string queryString)
{
@ -1249,4 +1256,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
}
}
}
}

View File

@ -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&parameter[correlation]=2&parameter[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&parameter[0].Value.Id=10")]
[InlineData("?parameter.index=low&parameter[low].Key=key0&parameter[low].Value.Id=10")]
[InlineData("?parameter.index=index&parameter[index].Key=key0&parameter[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&parameter[0].Value.Id=100")]
[InlineData("?parameter.index=low&parameter[low].Key=key0&parameter[low].Value.Id=100")]
[InlineData("?parameter.index=index&parameter[index].Key=key0&parameter[index].Value.Id=100")]
public async Task DictionaryModelBinder_BindsDictionaryOfComplexType_ImpliedPrefix_FindsValidationErrors(
string queryString)
{
@ -757,4 +943,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
}
}
}
}