Create model in `ComplexTypeModelBinder` if ANY property has a greedy binding source
- #7562 part 1
This commit is contained in:
parent
6abb4d9e81
commit
c13e2498a8
|
|
@ -216,8 +216,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Any of the model properties can be bound using a value provider.
|
// 2. Any of the model properties can be bound.
|
||||||
if (CanValueBindAnyModelProperties(bindingContext))
|
if (CanBindAnyModelProperties(bindingContext))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanValueBindAnyModelProperties(ModelBindingContext bindingContext)
|
private bool 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
|
// 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.
|
// level object. So we return false.
|
||||||
|
|
@ -235,20 +235,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to check to see if any of the properties of the model can be bound using the value providers,
|
// We want to check to see if any of the properties of the model can be bound using the value providers or
|
||||||
// because that's all that MutableObjectModelBinder can handle.
|
// a greedy binder.
|
||||||
//
|
//
|
||||||
// However, because a property might specify a custom binding source ([FromForm]), it's not correct
|
// Because a property might specify a custom binding source ([FromForm]), it's not correct
|
||||||
// for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName),
|
// for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
|
||||||
// because that may include other value providers - that would lead us to mistakenly create the model
|
// that may include other value providers - that would lead us to mistakenly create the model
|
||||||
// when the data is coming from a source we should use (ex: value found in query string, but the
|
// when the data is coming from a source we should use (ex: value found in query string, but the
|
||||||
// model has [FromForm]).
|
// model has [FromForm]).
|
||||||
//
|
//
|
||||||
// To do this we need to enumerate the properties, and see which of them provide a binding source
|
// To do this we need to enumerate the properties, and see which of them provide a binding source
|
||||||
// through metadata, then we decide what to do.
|
// through metadata, then we decide what to do.
|
||||||
//
|
//
|
||||||
// If a property has a binding source, and it's a greedy source, then it's not
|
// If a property has a binding source, and it's a greedy source, then it's always bound.
|
||||||
// allowed to come from a value provider, so we skip it.
|
|
||||||
//
|
//
|
||||||
// If a property has a binding source, and it's a non-greedy source, then we'll filter the
|
// If a property has a binding source, and it's a non-greedy source, then we'll filter the
|
||||||
// the value providers to just that source, and see if we can find a matching prefix
|
// the value providers to just that source, and see if we can find a matching prefix
|
||||||
|
|
@ -256,12 +255,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
//
|
//
|
||||||
// If a property does not have a binding source, then it's fair game for any value provider.
|
// If a property does not have a binding source, then it's fair game for any value provider.
|
||||||
//
|
//
|
||||||
// If any property meets the above conditions and has a value from ValueProviders, then we'll
|
// 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. OR if ALL properties of the model have a greedy source,
|
// 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.
|
// then we go ahead and create it.
|
||||||
//
|
//
|
||||||
var hasBindableProperty = false;
|
|
||||||
var isAnyPropertyEnabledForValueProviderBasedBinding = false;
|
|
||||||
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
|
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
|
||||||
{
|
{
|
||||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
|
var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
|
||||||
|
|
@ -270,41 +267,30 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBindableProperty = true;
|
// If any property can be bound from a greedy binding source, then success.
|
||||||
|
|
||||||
// This check will skip properties which are marked explicitly using a non value binder.
|
|
||||||
var bindingSource = propertyMetadata.BindingSource;
|
var bindingSource = propertyMetadata.BindingSource;
|
||||||
if (bindingSource == null || !bindingSource.IsGreedy)
|
if (bindingSource != null && bindingSource.IsGreedy)
|
||||||
{
|
{
|
||||||
isAnyPropertyEnabledForValueProviderBasedBinding = true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var fieldName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
|
// Otherwise, check whether the (perhaps filtered) value providers have a match.
|
||||||
var modelName = ModelNames.CreatePropertyModelName(
|
var fieldName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
|
||||||
bindingContext.ModelName,
|
var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
|
||||||
fieldName);
|
using (bindingContext.EnterNestedScope(
|
||||||
|
modelMetadata: propertyMetadata,
|
||||||
using (bindingContext.EnterNestedScope(
|
fieldName: fieldName,
|
||||||
modelMetadata: propertyMetadata,
|
modelName: modelName,
|
||||||
fieldName: fieldName,
|
model: null))
|
||||||
modelName: modelName,
|
{
|
||||||
model: null))
|
// If any property can be bound from a value provider, then success.
|
||||||
|
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
|
||||||
{
|
{
|
||||||
// If any property can be bound from a value provider then continue.
|
return true;
|
||||||
if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasBindableProperty && !isAnyPropertyEnabledForValueProviderBasedBinding)
|
|
||||||
{
|
|
||||||
// All the properties are marked with a non value provider based marker like [FromHeader] or
|
|
||||||
// [FromBody].
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.CannotBindToComplexType(bindingContext);
|
_logger.CannotBindToComplexType(bindingContext);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -155,10 +155,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
||||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
|
||||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
|
||||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
|
|
||||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
|
|
||||||
public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue(
|
public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfAValueProviderProvidesValue(
|
||||||
Type modelType,
|
Type modelType,
|
||||||
bool valueProviderProvidesValue)
|
bool valueProviderProvidesValue)
|
||||||
|
|
@ -182,6 +178,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Equal(valueProviderProvidesValue, canCreate);
|
Assert.Equal(valueProviderProvidesValue, canCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
||||||
|
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
||||||
|
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
|
||||||
|
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
|
||||||
|
public void CanCreateModel_CreatesModelForValueProviderBasedBinderMetadatas_IfPropertyHasGreedyBindingSource(
|
||||||
|
Type modelType,
|
||||||
|
bool valueProviderProvidesValue)
|
||||||
|
{
|
||||||
|
var valueProvider = new Mock<IValueProvider>();
|
||||||
|
valueProvider
|
||||||
|
.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
|
||||||
|
.Returns(valueProviderProvidesValue);
|
||||||
|
|
||||||
|
var bindingContext = CreateContext(GetMetadataForType(modelType));
|
||||||
|
bindingContext.IsTopLevelObject = false;
|
||||||
|
bindingContext.ValueProvider = valueProvider.Object;
|
||||||
|
bindingContext.OriginalValueProvider = valueProvider.Object;
|
||||||
|
|
||||||
|
var binder = CreateBinder(bindingContext.ModelMetadata);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var canCreate = binder.CanCreateModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(canCreate);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
|
||||||
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
|
||||||
|
|
@ -214,17 +238,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
var canCreate = binder.CanCreateModel(bindingContext);
|
var canCreate = binder.CanCreateModel(bindingContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(originalValueProviderProvidesValue, canCreate);
|
Assert.True(canCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false)]
|
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), false, true)]
|
||||||
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true)]
|
[InlineData(typeof(TypeWithUnmarkedAndBinderMetadataMarkedProperties), true, true)]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), false, false)]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), true, true)]
|
||||||
public void CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(
|
public void CanCreateModel_UnmarkedProperties_UsesCurrentValueProvider(
|
||||||
Type modelType,
|
Type modelType,
|
||||||
bool valueProviderProvidesValue)
|
bool valueProviderProvidesValue,
|
||||||
|
bool expectedCanCreate)
|
||||||
{
|
{
|
||||||
var valueProvider = new Mock<IValueProvider>();
|
var valueProvider = new Mock<IValueProvider>();
|
||||||
valueProvider
|
valueProvider
|
||||||
|
|
@ -247,7 +272,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
var canCreate = binder.CanCreateModel(bindingContext);
|
var canCreate = binder.CanCreateModel(bindingContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(valueProviderProvidesValue, canCreate);
|
Assert.Equal(expectedCanCreate, canCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
{
|
{
|
||||||
// Integration tests targeting the behavior of the MutableObjectModelBinder and related classes
|
// Integration tests targeting the behavior of the ComplexTypeModelBinder and related classes
|
||||||
// with other model binders.
|
// with other model binders.
|
||||||
public class ComplexTypeModelBinderIntegrationTest
|
public class ComplexTypeModelBinderIntegrationTest
|
||||||
{
|
{
|
||||||
|
|
@ -197,10 +197,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal("bill", entry.RawValue);
|
Assert.Equal("bill", entry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't provide enough data in this test for the 'Person' model to be created. So even though there is
|
|
||||||
// body data in the request, it won't be used.
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_PartialData()
|
public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_PartialData()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
|
|
@ -235,22 +233,21 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.True(modelBindingResult.IsModelSet);
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
var model = Assert.IsType<Order1>(modelBindingResult.Model);
|
var model = Assert.IsType<Order1>(modelBindingResult.Model);
|
||||||
Assert.Null(model.Customer);
|
Assert.NotNull(model.Customer);
|
||||||
|
Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
|
||||||
|
|
||||||
Assert.Equal(10, model.ProductId);
|
Assert.Equal(10, model.ProductId);
|
||||||
|
|
||||||
Assert.Single(modelState);
|
|
||||||
Assert.Equal(0, modelState.ErrorCount);
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
|
|
||||||
var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
|
var entry = Assert.Single(modelState).Value;
|
||||||
Assert.Equal("10", entry.AttemptedValue);
|
Assert.Equal("10", entry.AttemptedValue);
|
||||||
Assert.Equal("10", entry.RawValue);
|
Assert.Equal("10", entry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't provide enough data in this test for the 'Person' model to be created. So even though there is
|
|
||||||
// body data in the request, it won't be used.
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoData()
|
public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoData()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
|
|
@ -285,7 +282,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.True(modelBindingResult.IsModelSet);
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
var model = Assert.IsType<Order1>(modelBindingResult.Model);
|
var model = Assert.IsType<Order1>(modelBindingResult.Model);
|
||||||
Assert.Null(model.Customer);
|
Assert.NotNull(model.Customer);
|
||||||
|
Assert.Equal("1 Microsoft Way", model.Customer.Address.Street);
|
||||||
|
|
||||||
Assert.Empty(modelState);
|
Assert.Empty(modelState);
|
||||||
Assert.Equal(0, modelState.ErrorCount);
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
|
|
@ -630,10 +628,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal("bill", entry.RawValue);
|
Assert.Equal("bill", entry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't provide enough data in this test for the 'Person' model to be created. So even though there are
|
|
||||||
// form files in the request, it won't be used.
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_PartialData()
|
public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_PartialData()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
|
|
@ -668,22 +664,29 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.True(modelBindingResult.IsModelSet);
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
var model = Assert.IsType<Order4>(modelBindingResult.Model);
|
var model = Assert.IsType<Order4>(modelBindingResult.Model);
|
||||||
Assert.Null(model.Customer);
|
Assert.NotNull(model.Customer);
|
||||||
|
|
||||||
|
var document = Assert.Single(model.Customer.Documents);
|
||||||
|
Assert.Equal("text.txt", document.FileName);
|
||||||
|
using (var reader = new StreamReader(document.OpenReadStream()))
|
||||||
|
{
|
||||||
|
Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
|
||||||
|
}
|
||||||
|
|
||||||
Assert.Equal(10, model.ProductId);
|
Assert.Equal(10, model.ProductId);
|
||||||
|
|
||||||
Assert.Single(modelState);
|
Assert.Equal(2, modelState.Count);
|
||||||
Assert.Equal(0, modelState.ErrorCount);
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
|
|
||||||
|
Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents");
|
||||||
var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
|
var entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId").Value;
|
||||||
Assert.Equal("10", entry.AttemptedValue);
|
Assert.Equal("10", entry.AttemptedValue);
|
||||||
Assert.Equal("10", entry.RawValue);
|
Assert.Equal("10", entry.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't provide enough data in this test for the 'Person' model to be created. So even though there is
|
|
||||||
// body data in the request, it won't be used.
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoData()
|
public async Task ComplexTypeModelBinder_BindsNestedPOCO_WithFormFileModelBinder_WithPrefix_NoData()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameter = new ParameterDescriptor()
|
var parameter = new ParameterDescriptor()
|
||||||
|
|
@ -696,7 +699,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
{
|
{
|
||||||
request.QueryString = new QueryString("?");
|
request.QueryString = new QueryString("?");
|
||||||
SetFormFileBodyContent(request, "Hello, World!", "parameter.Customer.Documents");
|
SetFormFileBodyContent(request, "Hello, World!", "Customer.Documents");
|
||||||
});
|
});
|
||||||
|
|
||||||
var modelState = testContext.ModelState;
|
var modelState = testContext.ModelState;
|
||||||
|
|
@ -718,11 +721,20 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.True(modelBindingResult.IsModelSet);
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
var model = Assert.IsType<Order4>(modelBindingResult.Model);
|
var model = Assert.IsType<Order4>(modelBindingResult.Model);
|
||||||
Assert.Null(model.Customer);
|
Assert.NotNull(model.Customer);
|
||||||
|
|
||||||
|
var document = Assert.Single(model.Customer.Documents);
|
||||||
|
Assert.Equal("text.txt", document.FileName);
|
||||||
|
using (var reader = new StreamReader(document.OpenReadStream()))
|
||||||
|
{
|
||||||
|
Assert.Equal("Hello, World!", await reader.ReadToEndAsync());
|
||||||
|
}
|
||||||
|
|
||||||
Assert.Empty(modelState);
|
|
||||||
Assert.Equal(0, modelState.ErrorCount);
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
Assert.True(modelState.IsValid);
|
Assert.True(modelState.IsValid);
|
||||||
|
|
||||||
|
var entry = Assert.Single(modelState);
|
||||||
|
Assert.Equal("Customer.Documents", entry.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Order5
|
private class Order5
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue