Merge pull request #8491 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Doug Bunting 2018-09-21 15:40:49 -07:00 committed by GitHub
commit 90089953d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 843 additions and 126 deletions

View File

@ -14,7 +14,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// Error message the model binding system adds when a property with an associated
/// <c>BindRequiredAttribute</c> is not bound.
/// </summary>
/// <value>Default <see cref="string"/> is "A value for the '{0}' property was not provided.".</value>
/// <value>
/// Default <see cref="string"/> is "A value for the '{0}' parameter or property was not provided.".
/// </value>
public virtual Func<string, string> MissingBindRequiredValueAccessor { get; }
/// <summary>

View File

@ -38,11 +38,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public ArrayModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory)
: base(elementBinder, loggerFactory)
{
}
/// <summary>
/// Creates a new <see cref="ArrayModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public ArrayModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
: base(elementBinder, loggerFactory, allowValidatingTopLevelNodes)
{
}
/// <inheritdoc />
public override bool CanCreateInstance(Type targetType)
{

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -27,7 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory);
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
return null;

View File

@ -44,7 +44,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public CollectionModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory)
: this(elementBinder, loggerFactory, allowValidatingTopLevelNodes: false)
{
}
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public CollectionModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
{
if (elementBinder == null)
{
@ -58,8 +80,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
ElementBinder = elementBinder;
Logger = loggerFactory.CreateLogger(GetType());
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
}
// Internal for testing.
internal bool AllowValidatingTopLevelNodes { get; }
/// <summary>
/// Gets the <see cref="IModelBinder"/> instances for binding collection elements.
/// </summary>
@ -94,6 +120,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
model = CreateEmptyCollection(bindingContext.ModelType);
}
if (AllowValidatingTopLevelNodes)
{
AddErrorIfBindingRequired(bindingContext);
}
bindingContext.Result = ModelBindingResult.Success(model);
}
@ -161,6 +192,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
typeof(ICollection<TElement>).IsAssignableFrom(targetType);
}
/// <summary>
/// Add a <see cref="ModelError" /> to <see cref="ModelBindingContext.ModelState" /> if
/// <see cref="ModelMetadata.IsBindingRequired" />.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <remarks>
/// <para>
/// This method should be called only when <see cref="MvcOptions.AllowValidatingTopLevelNodes" /> is
/// <see langword="true" /> and a top-level model was not bound.
/// </para>
/// <para>
/// For back-compatibility reasons, <see cref="ModelBindingContext.Result" /> must have
/// <see cref="ModelBindingResult.IsModelSet" /> equal to <see langword="true" /> when a
/// top-level model is not bound. Therefore, ParameterBinder can not detect a
/// <see cref="ModelMetadata.IsBindingRequired" /> failure for collections. Add the error here.
/// </para>
/// </remarks>
protected void AddErrorIfBindingRequired(ModelBindingContext bindingContext)
{
var modelMetadata = bindingContext.ModelMetadata;
if (modelMetadata.IsBindingRequired)
{
var messageProvider = modelMetadata.ModelBindingMessageProvider;
var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
}
}
/// <summary>
/// Create an <see cref="object"/> assignable to <paramref name="targetType"/>.
/// </summary>

View File

@ -7,6 +7,7 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -32,6 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
// If the model type is ICollection<> then we can call its Add method, so we can always support it.
var collectionType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(ICollection<>));
@ -41,7 +43,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory);
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
// If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since
@ -57,7 +63,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(enumerableType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory);
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
}

View File

@ -45,9 +45,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public ComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
ILoggerFactory loggerFactory)
: this(propertyBinders, loggerFactory, allowValidatingTopLevelNodes: false)
{
}
/// <summary>
/// Creates a new <see cref="ComplexTypeModelBinder"/>.
/// </summary>
/// <param name="propertyBinders">
/// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public ComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
{
if (propertyBinders == null)
{
@ -61,8 +85,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
_propertyBinders = propertyBinders;
_logger = loggerFactory.CreateLogger<ComplexTypeModelBinder>();
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
}
// Internal for testing.
internal bool AllowValidatingTopLevelNodes { get; }
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -91,9 +119,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.Model = CreateModel(bindingContext);
}
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
var modelMetadata = bindingContext.ModelMetadata;
var attemptedPropertyBinding = false;
for (var i = 0; i < modelMetadata.Properties.Count; i++)
{
var property = bindingContext.ModelMetadata.Properties[i];
var property = modelMetadata.Properties[i];
if (!CanBindProperty(bindingContext, property))
{
continue;
@ -127,15 +157,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (result.IsModelSet)
{
attemptedPropertyBinding = true;
SetProperty(bindingContext, modelName, property, result);
}
else if (property.IsBindingRequired)
{
attemptedPropertyBinding = true;
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message);
}
}
// Have we created a top-level model despite an inability to bind anything in said model and a lack of
// other IsBindingRequired errors? Does that violate [BindRequired] on the model? This case occurs when
// 1. The top-level model has no public settable properties.
// 2. All properties in a [BindRequired] model have [BindNever] or are otherwise excluded from binding.
// 3. No data exists for any property.
if (AllowValidatingTopLevelNodes &&
!attemptedPropertyBinding &&
bindingContext.IsTopLevelObject &&
modelMetadata.IsBindingRequired)
{
var messageProvider = modelMetadata.ModelBindingMessageProvider;
var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
}
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
_logger.DoneAttemptingToBindModel(bindingContext);
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -31,7 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return new ComplexTypeModelBinder(
propertyBinders,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
return null;

View File

@ -43,6 +43,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public DictionaryModelBinder(IModelBinder keyBinder, IModelBinder valueBinder, ILoggerFactory loggerFactory)
: base(new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory), loggerFactory)
{
@ -54,6 +58,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
_valueBinder = valueBinder;
}
/// <summary>
/// Creates a new <see cref="DictionaryModelBinder{TKey, TValue}"/>.
/// </summary>
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public DictionaryModelBinder(
IModelBinder keyBinder,
IModelBinder valueBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
: base(
new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory),
loggerFactory,
// CollectionModelBinder should not check IsRequired, done in this model binder.
allowValidatingTopLevelNodes: false)
{
if (valueBinder == null)
{
throw new ArgumentNullException(nameof(valueBinder));
}
_valueBinder = valueBinder;
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
}
// Internal for testing.
internal new bool AllowValidatingTopLevelNodes { get; }
/// <inheritdoc />
public override async Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -85,6 +123,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
// No IEnumerableValueProvider available for the fallback approach. For example the user may have
// replaced the ValueProvider with something other than a CompositeValueProvider.
if (AllowValidatingTopLevelNodes && bindingContext.IsTopLevelObject)
{
AddErrorIfBindingRequired(bindingContext);
}
return;
}
@ -94,6 +137,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (keys.Count == 0)
{
// No entries with the expected keys.
if (AllowValidatingTopLevelNodes && bindingContext.IsTopLevelObject)
{
AddErrorIfBindingRequired(bindingContext);
}
return;
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -34,7 +35,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return (IModelBinder)Activator.CreateInstance(binderType, keyBinder, valueBinder, loggerFactory);
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
binderType,
keyBinder,
valueBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
return null;

View File

@ -795,7 +795,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
=> GetString("ModelBinderUtil_ModelMetadataCannotBeNull");
/// <summary>
/// A value for the '{0}' property was not provided.
/// A value for the '{0}' parameter or property was not provided.
/// </summary>
internal static string ModelBinding_MissingBindRequiredMember
{
@ -803,7 +803,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
}
/// <summary>
/// A value for the '{0}' property was not provided.
/// A value for the '{0}' parameter or property was not provided.
/// </summary>
internal static string FormatModelBinding_MissingBindRequiredMember(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_MissingBindRequiredMember"), p0);

View File

@ -295,7 +295,7 @@
<value>The binding context cannot have a null ModelMetadata.</value>
</data>
<data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve">
<value>A value for the '{0}' property was not provided.</value>
<value>A value for the '{0}' parameter or property was not provided.</value>
</data>
<data name="ModelBinding_MissingRequestBodyRequiredMember" xml:space="preserve">
<value>A non-empty request body is required.</value>

View File

@ -51,6 +51,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType(typeof(ArrayModelBinder<>).MakeGenericType(modelType.GetElementType()), result);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForArrayType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new ArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(int[]));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
Assert.Equal(typeof(int), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<ArrayModelBinder<int>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
[Fact]
public void Create_ForModelMetadataReadOnly_ReturnsNull()
{

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
@ -42,13 +43,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(new[] { 42, 84 }, array);
}
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
private IActionResult ActionWithArrayParameter(string[] parameter) => null;
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
@ -57,7 +66,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[]));
var parameter = typeof(ArrayModelBinderTest)
.GetMethod(nameof(ActionWithArrayParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -67,22 +82,74 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.Empty(Assert.IsType<string[]>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
[Fact]
public async Task ArrayModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
{
// Arrange
var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(ArrayModelBinderTest)
.GetMethod(nameof(ActionWithArrayParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<string[]>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
[Theory]
[InlineData("", false, false)]
[InlineData("", true, false)]
[InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ArrayProperty");
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(ModelWithArrayProperty), nameof(ModelWithArrayProperty.ArrayProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithArrayProperty),
nameof(ModelWithArrayProperty.ArrayProperty));
@ -94,6 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
public static TheoryData<int[]> ArrayModelData
@ -177,23 +245,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext GetBindingContext(IValueProvider valueProvider)
{
var bindingContext = new DefaultModelBindingContext()
{
ModelName = "someName",
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
};
var bindingContext = CreateContext();
bindingContext.ModelName = "someName";
bindingContext.ValueProvider = valueProvider;
return bindingContext;
}
private static DefaultModelBindingContext CreateContext()
{
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = new ActionContext()
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext(),
},
};
var modelBindingContext = new DefaultModelBindingContext
{
ActionContext = actionContext,
ModelState = actionContext.ModelState,
};
return modelBindingContext;

View File

@ -66,6 +66,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<CollectionModelBinder<int>>(result);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForSupportedType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new CollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(List<int>));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
Assert.Equal(typeof(int), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<CollectionModelBinder<int>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
private class Person
{
public string Name { get; set; }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
@ -211,13 +212,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Empty(boundCollection.Model);
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
private IActionResult ActionWithListParameter(List<string> parameter) => null;
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
@ -226,7 +235,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>));
var parameter = typeof(CollectionModelBinderTest)
.GetMethod(nameof(ActionWithListParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -236,6 +251,45 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.Empty(Assert.IsType<List<string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
{
// Arrange
var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(CollectionModelBinderTest)
.GetMethod(nameof(ActionWithListParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<List<string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
// Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject except
@ -272,19 +326,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
}
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
[InlineData("", false, false)]
[InlineData("", true, false)]
[InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(ModelWithListProperty), nameof(ModelWithListProperty.ListProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithListProperty),
nameof(ModelWithListProperty.ListProperty));
@ -296,6 +363,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
// Model type -> can create instance.
@ -365,15 +433,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
typeof(ModelWithIListProperty),
nameof(ModelWithIListProperty.ListProperty));
var bindingContext = new DefaultModelBindingContext
{
ModelMetadata = metadata,
ModelName = "someName",
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(),
FieldName = "testfieldname",
};
var bindingContext = CreateContext();
bindingContext.FieldName = "testfieldname";
bindingContext.ModelName = "someName";
bindingContext.ModelMetadata = metadata;
bindingContext.ValueProvider = valueProvider;
return bindingContext;
}
@ -412,12 +476,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext CreateContext()
{
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = new ActionContext()
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
},
};
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = actionContext,
ModelState = actionContext.ModelState,
ValidationState = new ValidationStateDictionary(),
};
return modelBindingContext;

View File

@ -55,6 +55,38 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<ComplexTypeModelBinder>(result);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForSupportedType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new ComplexTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<ComplexTypeModelBinder>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
private class Person
{
public string Name { get; set; }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -275,8 +276,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(expectedCanCreate, canCreate);
}
[Fact]
public async Task BindModelAsync_CreatesModel_IfIsTopLevelObject()
private IActionResult ActionWithComplexParameter(Person parameter) => null;
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task BindModelAsync_CreatesModel_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var mockValueProvider = new Mock<IValueProvider>();
@ -287,6 +295,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Mock binder fails to bind all properties.
var mockBinder = new StubModelBinder();
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
@ -298,7 +314,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var model = new Person();
var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
var testableBinder = new Mock<TestableComplexTypeModelBinder>(allowValidatingTopLevelNodes)
{
CallBase = true
};
testableBinder
.Setup(o => o.CreateModelPublic(bindingContext))
.Returns(model)
@ -312,11 +331,149 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
var returnedPerson = Assert.IsType<Person>(bindingContext.Result.Model);
Assert.Same(model, returnedPerson);
testableBinder.Verify();
}
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoData()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
// Mock binder fails to bind all properties.
var innerBinder = new StubModelBinder();
var binders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (var property in metadataProvider.GetMetadataForProperties(typeof(Person)))
{
binders.Add(property, innerBinder);
}
var binder = new ComplexTypeModelBinder(
binders,
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<Person>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
private IActionResult ActionWithNoSettablePropertiesParameter(PersonWithNoProperties parameter) => null;
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoSettableProperties()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(
nameof(ActionWithNoSettablePropertiesParameter),
BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
var binder = new ComplexTypeModelBinder(
new Dictionary<ModelMetadata, IModelBinder>(),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<PersonWithNoProperties>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
private IActionResult ActionWithAllPropertiesExcludedParameter(PersonWithAllPropertiesExcluded parameter) => null;
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithAllPropertiesExcluded()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(
nameof(ActionWithAllPropertiesExcludedParameter),
BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
var binder = new ComplexTypeModelBinder(
new Dictionary<ModelMetadata, IModelBinder>(),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<PersonWithAllPropertiesExcluded>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
[Theory]
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyInt), false)] // read-only value type
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), true)]
@ -644,7 +801,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var modelError = Assert.Single(entry.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
}
[Fact]
@ -678,7 +835,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var modelError = Assert.Single(entry.Errors);
Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
}
[Fact]
@ -1203,6 +1360,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public string name = null;
}
private class PersonWithAllPropertiesExcluded
{
[BindNever]
public DateTime DateOfBirth { get; set; }
[BindNever]
public DateTime? DateOfDeath { get; set; }
[BindNever]
public string FirstName { get; set; }
[BindNever]
public string LastName { get; set; }
public string NonUpdateableProperty { get; private set; }
}
private class PersonWithBindExclusion
{
[BindNever]
@ -1405,13 +1579,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
}
public TestableComplexTypeModelBinder(bool allowValidatingTopLevelNodes)
: this(new Dictionary<ModelMetadata, IModelBinder>(), allowValidatingTopLevelNodes)
{
}
public TestableComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
: base(propertyBinders, NullLoggerFactory.Instance)
{
Results = new Dictionary<ModelMetadata, ModelBindingResult>();
}
public Dictionary<ModelMetadata, ModelBindingResult> Results { get; }
public TestableComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
bool allowValidatingTopLevelNodes)
: base(propertyBinders, NullLoggerFactory.Instance, allowValidatingTopLevelNodes)
{
}
public Dictionary<ModelMetadata, ModelBindingResult> Results { get; } = new Dictionary<ModelMetadata, ModelBindingResult>();
public virtual Task BindPropertyPublic(ModelBindingContext bindingContext)
{

View File

@ -60,6 +60,39 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<DictionaryModelBinder<string, int>>(result);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForDictionaryType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new DictionaryModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Dictionary<string, string>));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(KeyValuePair<string, string>) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<DictionaryModelBinder<string, string>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
Assert.False(((CollectionModelBinder<KeyValuePair<string, string>>)binder).AllowValidatingTopLevelNodes);
}
private class Person
{
public string Name { get; set; }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
@ -343,14 +344,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(expectedDictionary, resultDictionary);
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
private IActionResult ActionWithDictionaryParameter(Dictionary<string, string> parameter) => null;
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new DictionaryModelBinder<string, string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
@ -359,7 +368,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>));
var parameter = typeof(DictionaryModelBinderTest)
.GetMethod(nameof(ActionWithDictionaryParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -369,23 +384,78 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.Empty(Assert.IsType<Dictionary<string, string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(DictionaryModelBinderTest)
.GetMethod(nameof(ActionWithDictionaryParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<Dictionary<string, string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
[Theory]
[InlineData("")]
[InlineData("param")]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
[InlineData("", false, false)]
[InlineData("", true, false)]
[InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new DictionaryModelBinder<int, int>(
new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(
typeof(ModelWithDictionaryProperties),
nameof(ModelWithDictionaryProperties.DictionaryProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperties),
nameof(ModelWithDictionaryProperties.DictionaryProperty));
@ -397,6 +467,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
// Model type -> can create instance.
@ -436,13 +507,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext CreateContext()
{
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = new ActionContext()
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
},
ModelState = new ModelStateDictionary(),
};
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = actionContext,
ModelState = actionContext.ModelState,
ValidationState = new ValidationStateDictionary(),
};
@ -495,14 +567,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
valueProvider.Add(kvp.Key, string.Empty);
}
var bindingContext = new DefaultModelBindingContext
{
ModelMetadata = metadata,
ModelName = "someName",
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(),
};
var bindingContext = CreateContext();
bindingContext.ModelMetadata = metadata;
bindingContext.ModelName = "someName";
bindingContext.ValueProvider = valueProvider;
return bindingContext;
}

View File

@ -36,13 +36,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
BindingSource = Metadata.BindingSource,
PropertyFilterProvider = Metadata.PropertyFilterProvider,
};
Services = GetServices();
(Services, MvcOptions) = GetServicesAndOptions();
}
public override BindingInfo BindingInfo => _bindingInfo;
public override ModelMetadata Metadata { get; }
public MvcOptions MvcOptions { get; }
public override IModelMetadataProvider MetadataProvider { get; }
public override IServiceProvider Services { get; }
@ -77,12 +80,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
_binderCreators.Add((m) => m.Equals(metadata) ? binderCreator() : null);
}
private static IServiceProvider GetServices()
private static (IServiceProvider, MvcOptions) GetServicesAndOptions()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
services.AddSingleton(Options.Create(new MvcOptions()));
return services.BuildServiceProvider();
var mvcOptions = new MvcOptions();
services.AddSingleton(Options.Create(mvcOptions));
return (services.BuildServiceProvider(), mvcOptions);
}
}
}

View File

@ -89,10 +89,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"The RequiredProp field is required.",
errors["RequiredProp"]);
Assert.Equal(
"A value for the 'BindRequiredProp' property was not provided.",
"A value for the 'BindRequiredProp' parameter or property was not provided.",
errors["BindRequiredProp"]);
Assert.Equal(
"A value for the 'RequiredAndBindRequiredProp' property was not provided.",
"A value for the 'RequiredAndBindRequiredProp' parameter or property was not provided.",
errors["RequiredAndBindRequiredProp"]);
Assert.Equal(
"The field OptionalStringLengthProp must be a string with a maximum length of 5.",
@ -104,10 +104,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"The requiredParam field is required.",
errors["requiredParam"]);
Assert.Equal(
"A value for the 'bindRequiredParam' property was not provided.",
"A value for the 'bindRequiredParam' parameter or property was not provided.",
errors["bindRequiredParam"]);
Assert.Equal(
"A value for the 'requiredAndBindRequiredParam' property was not provided.",
"A value for the 'requiredAndBindRequiredParam' parameter or property was not provided.",
errors["requiredAndBindRequiredParam"]);
Assert.Equal(
"The field optionalStringLengthParam must be a string with a maximum length of 5.",

View File

@ -333,13 +333,13 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
var error = Assert.Single(entry.Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1].Name").Value;
Assert.Null(entry.RawValue);
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
error = Assert.Single(entry.Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]

View File

@ -2123,7 +2123,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["Customer"].Errors);
Assert.Equal("A value for the 'Customer' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Customer' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2245,7 +2245,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["parameter.Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2299,7 +2299,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2357,7 +2357,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
}
private class Order12
@ -2411,7 +2411,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["ProductName"].Errors);
Assert.Equal("A value for the 'ProductName' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2463,7 +2463,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.ProductName"].Errors);
Assert.Equal("A value for the 'ProductName' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2563,7 +2563,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["OrderIds"].Errors);
Assert.Equal("A value for the 'OrderIds' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]
@ -2615,7 +2615,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.OrderIds"].Errors);
Assert.Equal("A value for the 'OrderIds' property was not provided.", error.ErrorMessage);
Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
}
[Fact]