Change `CollectionModelBinder` and `ComplexTypeModelBinder` to enforce `[BindRequired]`

- #8180
- add an error when binding fails for top-level model
  - same case as when MVC creates "default" / empty model i.e. `ParameterBinder` can't detect this
- update `CollectionModelBinder` subclasses and the various providers as well
- controlled by existing `MvcOptions.AllowValidatingTopLevelNodes` option

smaller issue:
- change `ModelBinding_MissingBindRequiredMember` resource to mention parameters too
This commit is contained in:
Doug Bunting 2018-09-09 21:48:23 -07:00
parent 61386d5f67
commit 5c8dfef15e
No known key found for this signature in database
GPG Key ID: 888B4EB7822B32E9
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,7 +33,8 @@ 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<>));
if (collectionType != null)
@ -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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -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()
var actionContext = new ActionContext
{
ActionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
},
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 actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
};
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
},
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 actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
};
var modelBindingContext = new DefaultModelBindingContext()
{
ActionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
},
ModelState = new ModelStateDictionary(),
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]