Fail `ComplexTypeModelBinder` after `CanCreateModel(...)` in some cases (#6793)
- #4802 and #6616 - also reduces the impact incorrect metadata as in #4939 - postpone some property binding in `ComplexTypeModelBinder`
This commit is contained in:
parent
de74a0e2f0
commit
5bbf7109a5
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -17,6 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
/// </summary>
|
||||
public class ComplexTypeModelBinder : IModelBinder
|
||||
{
|
||||
// Don't want a new public enum because communication between the private and internal methods of this class
|
||||
// should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.
|
||||
|
||||
// Model contains only properties that are expected to bind from value providers and no value provider has
|
||||
// matching data.
|
||||
internal const int NoDataAvailable = 0;
|
||||
// If model contains properties that are expected to bind from value providers, no value provider has matching
|
||||
// data. Remaining (greedy) properties might bind successfully.
|
||||
internal const int GreedyPropertiesMayHaveData = 1;
|
||||
// Model contains at least one property that is expected to bind from value providers and a value provider has
|
||||
// matching data.
|
||||
internal const int ValueProviderDataAvailable = 2;
|
||||
|
||||
private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
|
||||
private readonly ILogger _logger;
|
||||
private Func<object> _modelCreator;
|
||||
|
|
@ -76,18 +90,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
_logger.AttemptingToBindModel(bindingContext);
|
||||
|
||||
if (!CanCreateModel(bindingContext))
|
||||
var propertyData = CanCreateModel(bindingContext);
|
||||
if (propertyData == NoDataAvailable)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Perf: separated to avoid allocating a state machine when we don't
|
||||
// need to go async.
|
||||
return BindModelCoreAsync(bindingContext);
|
||||
return BindModelCoreAsync(bindingContext, propertyData);
|
||||
}
|
||||
|
||||
private async Task BindModelCoreAsync(ModelBindingContext bindingContext)
|
||||
private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int propertyData)
|
||||
{
|
||||
Debug.Assert(propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable);
|
||||
|
||||
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
|
||||
if (bindingContext.Model == null)
|
||||
{
|
||||
|
|
@ -96,6 +113,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
var attemptedPropertyBinding = false;
|
||||
var propertyBindingSucceeded = false;
|
||||
var postponePlaceholderBinding = false;
|
||||
for (var i = 0; i < modelMetadata.Properties.Count; i++)
|
||||
{
|
||||
var property = modelMetadata.Properties[i];
|
||||
|
|
@ -104,42 +123,62 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
continue;
|
||||
}
|
||||
|
||||
// Pass complex (including collection) values down so that binding system does not unnecessarily
|
||||
// recreate instances or overwrite inner properties that are not bound. No need for this with simple
|
||||
// values because they will be overwritten if binding succeeds. Arrays are never reused because they
|
||||
// cannot be resized.
|
||||
object propertyModel = null;
|
||||
if (property.PropertyGetter != null &&
|
||||
property.IsComplexType &&
|
||||
!property.ModelType.IsArray)
|
||||
if (_propertyBinders[property] is PlaceholderBinder)
|
||||
{
|
||||
propertyModel = property.PropertyGetter(bindingContext.Model);
|
||||
if (postponePlaceholderBinding)
|
||||
{
|
||||
// Decided to postpone binding properties that complete a loop in the model types when handling
|
||||
// an earlier loop-completing property. Postpone binding this property too.
|
||||
continue;
|
||||
}
|
||||
else if (!bindingContext.IsTopLevelObject &&
|
||||
!propertyBindingSucceeded &&
|
||||
propertyData == GreedyPropertiesMayHaveData)
|
||||
{
|
||||
// Have no confirmation of data for the current instance. Postpone completing the loop until
|
||||
// we _know_ the current instance is useful. Recursion would otherwise occur prior to the
|
||||
// block with a similar condition after the loop.
|
||||
//
|
||||
// Example cases include an Employee class containing
|
||||
// 1. a Manager property of type Employee
|
||||
// 2. an Employees property of type IList<Employee>
|
||||
postponePlaceholderBinding = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var fieldName = property.BinderModelName ?? property.PropertyName;
|
||||
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
|
||||
|
||||
ModelBindingResult result;
|
||||
using (bindingContext.EnterNestedScope(
|
||||
modelMetadata: property,
|
||||
fieldName: fieldName,
|
||||
modelName: modelName,
|
||||
model: propertyModel))
|
||||
{
|
||||
await BindProperty(bindingContext);
|
||||
result = bindingContext.Result;
|
||||
}
|
||||
var result = await BindProperty(bindingContext, property, fieldName, modelName);
|
||||
|
||||
if (result.IsModelSet)
|
||||
{
|
||||
attemptedPropertyBinding = true;
|
||||
SetProperty(bindingContext, modelName, property, result);
|
||||
propertyBindingSucceeded = true;
|
||||
}
|
||||
else if (property.IsBindingRequired)
|
||||
{
|
||||
attemptedPropertyBinding = true;
|
||||
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
|
||||
bindingContext.ModelState.TryAddModelError(modelName, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (postponePlaceholderBinding && propertyBindingSucceeded)
|
||||
{
|
||||
// Have some data for this instance. Continue with the model type loop.
|
||||
for (var i = 0; i < modelMetadata.Properties.Count; i++)
|
||||
{
|
||||
var property = modelMetadata.Properties[i];
|
||||
if (!CanBindProperty(bindingContext, property))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_propertyBinders[property] is PlaceholderBinder)
|
||||
{
|
||||
var fieldName = property.BinderModelName ?? property.PropertyName;
|
||||
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
|
||||
await BindProperty(bindingContext, property, fieldName, modelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +196,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
|
||||
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||
|
||||
// Have all binders failed because no data was available?
|
||||
//
|
||||
// If CanCreateModel determined a property has data, failures are likely due to conversion errors. For
|
||||
// example, user may submit ?[0].id=twenty&[1].id=twenty-one&[2].id=22 for a collection of a complex type
|
||||
// with an int id property. In that case, the bound model should be [ {}, {}, { id = 22 }] and
|
||||
// ModelState should contain errors about both [0].id and [1].id. Do not inform higher-level binders of the
|
||||
// failure in this and similar cases.
|
||||
//
|
||||
// If CanCreateModel could not find data for non-greedy properties, failures indicate greedy binders were
|
||||
// unsuccessful. For example, user may submit file attachments [0].File and [1].File but not [2].File for
|
||||
// a collection of a complex type containing an IFormFile property. In that case, we have exhausted the
|
||||
// attached files and checking for [3].File is likely be pointless. (And, if it had a point, would we stop
|
||||
// after 10 failures, 100, or more -- all adding redundant errors to ModelState?) Inform higher-level
|
||||
// binders of the failure.
|
||||
//
|
||||
// Required properties do not change the logic below. Missed required properties cause ModelState errors
|
||||
// but do not necessarily prevent further attempts to bind.
|
||||
//
|
||||
// This logic is intended to maximize correctness but does not avoid infinite loops or recursion when a
|
||||
// greedy model binder succeeds unconditionally.
|
||||
if (!bindingContext.IsTopLevelObject &&
|
||||
!propertyBindingSucceeded &&
|
||||
propertyData == GreedyPropertiesMayHaveData)
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Failed();
|
||||
return;
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -194,6 +262,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task<ModelBindingResult> BindProperty(
|
||||
ModelBindingContext bindingContext,
|
||||
ModelMetadata property,
|
||||
string fieldName,
|
||||
string modelName)
|
||||
{
|
||||
// Pass complex (including collection) values down so that binding system does not unnecessarily
|
||||
// recreate instances or overwrite inner properties that are not bound. No need for this with simple
|
||||
// values because they will be overwritten if binding succeeds. Arrays are never reused because they
|
||||
// cannot be resized.
|
||||
object propertyModel = null;
|
||||
if (property.PropertyGetter != null &&
|
||||
property.IsComplexType &&
|
||||
!property.ModelType.IsArray)
|
||||
{
|
||||
propertyModel = property.PropertyGetter(bindingContext.Model);
|
||||
}
|
||||
|
||||
ModelBindingResult result;
|
||||
using (bindingContext.EnterNestedScope(
|
||||
modelMetadata: property,
|
||||
fieldName: fieldName,
|
||||
modelName: modelName,
|
||||
model: propertyModel))
|
||||
{
|
||||
await BindProperty(bindingContext);
|
||||
result = bindingContext.Result;
|
||||
}
|
||||
|
||||
if (result.IsModelSet)
|
||||
{
|
||||
SetProperty(bindingContext, modelName, property, result);
|
||||
}
|
||||
else if (property.IsBindingRequired)
|
||||
{
|
||||
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
|
||||
bindingContext.ModelState.TryAddModelError(modelName, message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to bind a property of the model.
|
||||
/// </summary>
|
||||
|
|
@ -208,7 +318,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
return binder.BindModelAsync(bindingContext);
|
||||
}
|
||||
|
||||
internal bool CanCreateModel(ModelBindingContext bindingContext)
|
||||
internal int CanCreateModel(ModelBindingContext bindingContext)
|
||||
{
|
||||
var isTopLevelObject = bindingContext.IsTopLevelObject;
|
||||
|
||||
|
|
@ -227,33 +337,28 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var bindingSource = bindingContext.BindingSource;
|
||||
if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
|
||||
{
|
||||
return false;
|
||||
return NoDataAvailable;
|
||||
}
|
||||
|
||||
// Create the object if:
|
||||
// 1. It is a top level model.
|
||||
if (isTopLevelObject)
|
||||
{
|
||||
return true;
|
||||
return ValueProviderDataAvailable;
|
||||
}
|
||||
|
||||
// 2. Any of the model properties can be bound.
|
||||
if (CanBindAnyModelProperties(bindingContext))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return CanBindAnyModelProperties(bindingContext);
|
||||
}
|
||||
|
||||
private bool CanBindAnyModelProperties(ModelBindingContext bindingContext)
|
||||
private int CanBindAnyModelProperties(ModelBindingContext bindingContext)
|
||||
{
|
||||
// If there are no properties on the model, there is nothing to bind. We are here means this is not a top
|
||||
// level object. So we return false.
|
||||
if (bindingContext.ModelMetadata.Properties.Count == 0)
|
||||
{
|
||||
_logger.NoPublicSettableProperties(bindingContext);
|
||||
return false;
|
||||
return NoDataAvailable;
|
||||
}
|
||||
|
||||
// We want to check to see if any of the properties of the model can be bound using the value providers or
|
||||
|
|
@ -279,7 +384,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
|
||||
// create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
|
||||
// then we go ahead and create it.
|
||||
//
|
||||
var hasGreedyBinders = false;
|
||||
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
|
||||
{
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
|
||||
|
|
@ -292,7 +397,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var bindingSource = propertyMetadata.BindingSource;
|
||||
if (bindingSource != null && bindingSource.IsGreedy)
|
||||
{
|
||||
return true;
|
||||
hasGreedyBinders = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, check whether the (perhaps filtered) value providers have a match.
|
||||
|
|
@ -307,14 +413,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// If any property can be bound from a value provider, then success.
|
||||
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
|
||||
{
|
||||
return true;
|
||||
return ValueProviderDataAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGreedyBinders)
|
||||
{
|
||||
return GreedyPropertiesMayHaveData;
|
||||
}
|
||||
|
||||
_logger.CannotBindToComplexType(bindingContext);
|
||||
|
||||
return false;
|
||||
return NoDataAvailable;
|
||||
}
|
||||
|
||||
// Internal for tests
|
||||
|
|
|
|||
|
|
@ -24,11 +24,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
private static readonly IModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true)]
|
||||
[InlineData(false, false)]
|
||||
public void CanCreateModel_ReturnsTrue_IfIsTopLevelObject(
|
||||
bool isTopLevelObject,
|
||||
bool expectedCanCreate)
|
||||
[InlineData(true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
[InlineData(false, ComplexTypeModelBinder.NoDataAvailable)]
|
||||
public void CanCreateModel_ReturnsTrue_IfIsTopLevelObject(bool isTopLevelObject, int expectedCanCreate)
|
||||
{
|
||||
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)));
|
||||
bindingContext.IsTopLevelObject = isTopLevelObject;
|
||||
|
|
@ -56,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(canCreate);
|
||||
Assert.Equal(ComplexTypeModelBinder.NoDataAvailable, canCreate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -71,16 +69,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(canCreate);
|
||||
Assert.Equal(ComplexTypeModelBinder.ValueProviderDataAvailable, canCreate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void CanCreateModel_CreatesModel_WithAllGreedyProperties(bool isTopLevelObject)
|
||||
[InlineData(ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
[InlineData(ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
|
||||
public void CanCreateModel_CreatesModel_WithAllGreedyProperties(int expectedCanCreate)
|
||||
{
|
||||
var bindingContext = CreateContext(GetMetadataForType(typeof(HasAllGreedyProperties)));
|
||||
bindingContext.IsTopLevelObject = isTopLevelObject;
|
||||
bindingContext.IsTopLevelObject = expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable;
|
||||
|
||||
var binder = CreateBinder(bindingContext.ModelMetadata);
|
||||
|
||||
|
|
@ -88,20 +86,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(canCreate);
|
||||
Assert.Equal(expectedCanCreate, canCreate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void CanCreateModel_ReturnsTrue_IfNotIsTopLevelObject_BasedOnValueAvailability(
|
||||
bool valueAvailable)
|
||||
[InlineData(ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
[InlineData(ComplexTypeModelBinder.NoDataAvailable)]
|
||||
public void CanCreateModel_ReturnsTrue_IfNotIsTopLevelObject_BasedOnValueAvailability(int valueAvailable)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
|
||||
valueProvider
|
||||
.Setup(provider => provider.ContainsPrefix("SimpleContainer.Simple.Name"))
|
||||
.Returns(valueAvailable);
|
||||
.Returns(valueAvailable == ComplexTypeModelBinder.ValueProviderDataAvailable);
|
||||
|
||||
var modelMetadata = GetMetadataForProperty(typeof(SimpleContainer), nameof(SimpleContainer.Simple));
|
||||
var bindingContext = CreateContext(modelMetadata);
|
||||
|
|
@ -133,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(canCreate);
|
||||
Assert.Equal(ComplexTypeModelBinder.NoDataAvailable, canCreate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -149,20 +146,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(canCreate);
|
||||
Assert.Equal(ComplexTypeModelBinder.ValueProviderDataAvailable, canCreate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), ComplexTypeModelBinder.NoDataAvailable)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue(
|
||||
Type modelType,
|
||||
bool valueProviderProvidesValue)
|
||||
int valueProviderProvidesValue)
|
||||
{
|
||||
var valueProvider = new Mock<IValueProvider>();
|
||||
valueProvider
|
||||
.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
|
||||
.Returns(valueProviderProvidesValue);
|
||||
.Returns(valueProviderProvidesValue == ComplexTypeModelBinder.ValueProviderDataAvailable);
|
||||
|
||||
var bindingContext = CreateContext(GetMetadataForType(modelType));
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
|
|
@ -179,18 +176,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfPropertyHasGreedyBindingSource(
|
||||
Type modelType,
|
||||
bool valueProviderProvidesValue)
|
||||
int expectedCanCreate)
|
||||
{
|
||||
var valueProvider = new Mock<IValueProvider>();
|
||||
valueProvider
|
||||
.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
|
||||
.Returns(valueProviderProvidesValue);
|
||||
.Returns(expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable);
|
||||
|
||||
var bindingContext = CreateContext(GetMetadataForType(modelType));
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
|
|
@ -203,15 +200,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(canCreate);
|
||||
Assert.Equal(expectedCanCreate, canCreate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
|
||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
public void CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider(
|
||||
Type modelType,
|
||||
bool originalValueProviderProvidesValue)
|
||||
int expectedCanCreate)
|
||||
{
|
||||
var valueProvider = new Mock<IValueProvider>();
|
||||
valueProvider
|
||||
|
|
@ -221,7 +218,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var originalValueProvider = new Mock<IBindingSourceValueProvider>();
|
||||
originalValueProvider
|
||||
.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
|
||||
.Returns(originalValueProviderProvidesValue);
|
||||
.Returns(expectedCanCreate == ComplexTypeModelBinder.ValueProviderDataAvailable);
|
||||
|
||||
originalValueProvider
|
||||
.Setup(o => o.Filter(It.IsAny<BindingSource>()))
|
||||
|
|
@ -238,18 +235,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var canCreate = binder.CanCreateModel(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(canCreate);
|
||||
Assert.Equal(expectedCanCreate, canCreate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false, true)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true, true)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), false, false)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), true, true)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false, ComplexTypeModelBinder.GreedyPropertiesMayHaveData)]
|
||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), false, ComplexTypeModelBinder.NoDataAvailable)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), true, ComplexTypeModelBinder.ValueProviderDataAvailable)]
|
||||
public void CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(
|
||||
Type modelType,
|
||||
bool valueProviderProvidesValue,
|
||||
bool expectedCanCreate)
|
||||
int expectedCanCreate)
|
||||
{
|
||||
var valueProvider = new Mock<IValueProvider>();
|
||||
valueProvider
|
||||
|
|
@ -602,8 +599,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person());
|
||||
var originalModel = bindingContext.Model;
|
||||
var binders = bindingContext.ModelMetadata.Properties.ToDictionary(
|
||||
keySelector: item => item,
|
||||
elementSelector: item => (IModelBinder)null);
|
||||
|
||||
var binder = new Mock<TestableComplexTypeModelBinder>() { CallBase = true };
|
||||
var binder = new Mock<TestableComplexTypeModelBinder>(binders) { CallBase = true };
|
||||
binder
|
||||
.Setup(b => b.CreateModelPublic(It.IsAny<ModelBindingContext>()))
|
||||
.Verifiable();
|
||||
|
|
@ -621,8 +621,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
{
|
||||
// Arrange
|
||||
var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), model: null);
|
||||
var binders = bindingContext.ModelMetadata.Properties.ToDictionary(
|
||||
keySelector: item => item,
|
||||
elementSelector: item => (IModelBinder)null);
|
||||
|
||||
var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true };
|
||||
var testableBinder = new Mock<TestableComplexTypeModelBinder>(binders) { CallBase = true };
|
||||
testableBinder
|
||||
.Setup(o => o.CreateModelPublic(bindingContext))
|
||||
.Returns(new Person())
|
||||
|
|
|
|||
|
|
@ -3141,6 +3141,276 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Equal("10,20", entry.RawValue);
|
||||
}
|
||||
|
||||
private class Person5
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public IFormFile Photo { get; set; }
|
||||
}
|
||||
|
||||
// Regression test for #4802.
|
||||
[Fact]
|
||||
public async Task ComplexTypeModelBinder_ReportsFailureToCollectionModelBinder()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(IList<Person5>),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||
{
|
||||
SetFormFileBodyContent(request, "Hello world!", "[0].Photo");
|
||||
|
||||
// CollectionModelBinder binds an empty collection when value providers are all empty.
|
||||
request.QueryString = new QueryString("?a=b");
|
||||
});
|
||||
|
||||
var modelState = testContext.ModelState;
|
||||
var metadata = GetMetadata(testContext, parameter);
|
||||
var modelBinder = GetModelBinder(testContext, parameter, metadata);
|
||||
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||
testContext,
|
||||
modelBinder,
|
||||
valueProvider,
|
||||
parameter,
|
||||
metadata,
|
||||
value: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
var model = Assert.IsType<List<Person5>>(modelBindingResult.Model);
|
||||
var person = Assert.Single(model);
|
||||
Assert.Null(person.Name);
|
||||
Assert.NotNull(person.Photo);
|
||||
using (var reader = new StreamReader(person.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello world!", await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
var state = Assert.Single(modelState);
|
||||
Assert.Equal("[0].Photo", state.Key);
|
||||
Assert.Null(state.Value.AttemptedValue);
|
||||
Assert.Empty(state.Value.Errors);
|
||||
Assert.Null(state.Value.RawValue);
|
||||
}
|
||||
|
||||
private class Person6
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public Person6 Mother { get; set; }
|
||||
|
||||
public IFormFile Photo { get; set; }
|
||||
}
|
||||
|
||||
// Regression test for #6616.
|
||||
[Fact]
|
||||
public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_NearTopLevel()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(Person6),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||
{
|
||||
SetFormFileBodyContent(request, "Hello world!", "Photo");
|
||||
});
|
||||
|
||||
var modelState = testContext.ModelState;
|
||||
var metadata = GetMetadata(testContext, parameter);
|
||||
var modelBinder = GetModelBinder(testContext, parameter, metadata);
|
||||
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||
testContext,
|
||||
modelBinder,
|
||||
valueProvider,
|
||||
parameter,
|
||||
metadata,
|
||||
value: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
var model = Assert.IsType<Person6>(modelBindingResult.Model);
|
||||
Assert.Null(model.Mother);
|
||||
Assert.Null(model.Name);
|
||||
Assert.NotNull(model.Photo);
|
||||
using (var reader = new StreamReader(model.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello world!", await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
var state = Assert.Single(modelState);
|
||||
Assert.Equal("Photo", state.Key);
|
||||
Assert.Null(state.Value.AttemptedValue);
|
||||
Assert.Empty(state.Value.Errors);
|
||||
Assert.Null(state.Value.RawValue);
|
||||
}
|
||||
|
||||
// Regression test for #6616.
|
||||
[Fact]
|
||||
public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(Person6),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||
{
|
||||
SetFormFileBodyContent(request, "Hello world!", "Photo");
|
||||
SetFormFileBodyContent(request, "Hello Mom!", "Mother.Photo");
|
||||
});
|
||||
|
||||
var modelState = testContext.ModelState;
|
||||
var metadata = GetMetadata(testContext, parameter);
|
||||
var modelBinder = GetModelBinder(testContext, parameter, metadata);
|
||||
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||
testContext,
|
||||
modelBinder,
|
||||
valueProvider,
|
||||
parameter,
|
||||
metadata,
|
||||
value: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
var model = Assert.IsType<Person6>(modelBindingResult.Model);
|
||||
Assert.NotNull(model.Mother);
|
||||
Assert.Null(model.Mother.Mother);
|
||||
Assert.NotNull(model.Mother.Photo);
|
||||
using (var reader = new StreamReader(model.Mother.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello Mom!", await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
Assert.Null(model.Name);
|
||||
Assert.NotNull(model.Photo);
|
||||
using (var reader = new StreamReader(model.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello world!", await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("Photo", kvp.Key);
|
||||
Assert.Null(kvp.Value.AttemptedValue);
|
||||
Assert.Empty(kvp.Value.Errors);
|
||||
Assert.Null(kvp.Value.RawValue);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("Mother.Photo", kvp.Key);
|
||||
Assert.Null(kvp.Value.AttemptedValue);
|
||||
Assert.Empty(kvp.Value.Errors);
|
||||
Assert.Null(kvp.Value.RawValue);
|
||||
});
|
||||
}
|
||||
|
||||
private class Person7
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public IList<Person7> Children { get; set; }
|
||||
|
||||
public IFormFile Photo { get; set; }
|
||||
}
|
||||
|
||||
// Regression test for #6616.
|
||||
[Fact]
|
||||
public async Task ComplexTypeModelBinder_ReportsFailureToComplexTypeModelBinder_ViaCollection()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(Person7),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||
{
|
||||
SetFormFileBodyContent(request, "Hello world!", "Photo");
|
||||
SetFormFileBodyContent(request, "Hello Fred!", "Children[0].Photo");
|
||||
SetFormFileBodyContent(request, "Hello Ginger!", "Children[1].Photo");
|
||||
|
||||
request.QueryString = new QueryString("?Children[0].Name=Fred&Children[1].Name=Ginger");
|
||||
});
|
||||
|
||||
var modelState = testContext.ModelState;
|
||||
var metadata = GetMetadata(testContext, parameter);
|
||||
var modelBinder = GetModelBinder(testContext, parameter, metadata);
|
||||
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||
testContext,
|
||||
modelBinder,
|
||||
valueProvider,
|
||||
parameter,
|
||||
metadata,
|
||||
value: null);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
var model = Assert.IsType<Person7>(modelBindingResult.Model);
|
||||
Assert.NotNull(model.Children);
|
||||
Assert.Collection(
|
||||
model.Children,
|
||||
item =>
|
||||
{
|
||||
Assert.Null(item.Children);
|
||||
Assert.Equal("Fred", item.Name);
|
||||
using (var reader = new StreamReader(item.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello Fred!", reader.ReadToEnd());
|
||||
}
|
||||
},
|
||||
item =>
|
||||
{
|
||||
Assert.Null(item.Children);
|
||||
Assert.Equal("Ginger", item.Name);
|
||||
using (var reader = new StreamReader(item.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello Ginger!", reader.ReadToEnd());
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Null(model.Name);
|
||||
Assert.NotNull(model.Photo);
|
||||
using (var reader = new StreamReader(model.Photo.OpenReadStream()))
|
||||
{
|
||||
Assert.Equal("Hello world!", await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
private static void SetJsonBodyContent(HttpRequest request, string content)
|
||||
{
|
||||
var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));
|
||||
|
|
@ -3151,18 +3421,32 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
private static void SetFormFileBodyContent(HttpRequest request, string content, string name)
|
||||
{
|
||||
const string fileName = "text.txt";
|
||||
var fileCollection = new FormFileCollection();
|
||||
var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
|
||||
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
|
||||
request.Form = formCollection;
|
||||
request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
|
||||
request.Headers["Content-Disposition"] = $"form-data; name={name}; filename={fileName}";
|
||||
|
||||
fileCollection.Add(new FormFile(memoryStream, 0, memoryStream.Length, name, fileName)
|
||||
FormFileCollection fileCollection;
|
||||
if (request.HasFormContentType)
|
||||
{
|
||||
Headers = request.Headers
|
||||
});
|
||||
// Do less work and do not overwrite previous information if called a second time.
|
||||
fileCollection = (FormFileCollection)request.Form.Files;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileCollection = new FormFileCollection();
|
||||
var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
|
||||
|
||||
request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
|
||||
request.Form = formCollection;
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
var file = new FormFile(memoryStream, 0, memoryStream.Length, name, fileName)
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
|
||||
// Do not move this up. Headers must be non-null before the ContentDisposition property is accessed.
|
||||
ContentDisposition = $"form-data; name={name}; filename={fileName}",
|
||||
};
|
||||
|
||||
fileCollection.Add(file);
|
||||
}
|
||||
|
||||
private ModelMetadata GetMetadata(ModelBindingTestContext context, ParameterDescriptor parameter)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
private class Address
|
||||
{
|
||||
[FromHeader(Name = "Header")]
|
||||
[Required]
|
||||
[BindRequired]
|
||||
public string Street { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task BindPropertyFromHeader_NoData_UsesFullPathAsKeyForModelStateErrors()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "A value for the 'Header' parameter or property was not provided.";
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
var key = Assert.Single(modelState.Keys);
|
||||
Assert.Equal("CustomParameter.Address.Header", key);
|
||||
var error = Assert.Single(modelState[key].Errors);
|
||||
Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage("Street"), error.ErrorMessage);
|
||||
Assert.Equal(expected, error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -277,7 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = modelType
|
||||
};
|
||||
|
||||
Action<HttpRequest> action = r => r.Headers.Add("CustomParameter", new[] { expectedAttemptedValue });
|
||||
void action(HttpRequest r) => r.Headers.Add("CustomParameter", new[] { expectedAttemptedValue });
|
||||
var testContext = GetModelBindingTestContext(action);
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext.HttpContext.RequestServices);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue