[Fixes #5801] Move call to validate constructor in ComplexTypeModelBinder into CreateModel
This commit is contained in:
parent
531c11df2a
commit
29647fda33
|
|
@ -48,28 +48,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return TaskCache.CompletedTask;
|
return TaskCache.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
|
|
||||||
// reflection does not provide information about the implicit parameterless constructor for a struct.
|
|
||||||
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
|
|
||||||
// compile fails to construct it.
|
|
||||||
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
|
|
||||||
if (bindingContext.Model == null &&
|
|
||||||
(modelTypeInfo.IsAbstract ||
|
|
||||||
modelTypeInfo.GetConstructor(Type.EmptyTypes) == null))
|
|
||||||
{
|
|
||||||
if (bindingContext.IsTopLevelObject)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject(modelTypeInfo.FullName));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty(
|
|
||||||
modelTypeInfo.FullName,
|
|
||||||
bindingContext.ModelName,
|
|
||||||
bindingContext.ModelMetadata.ContainerType.FullName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perf: separated to avoid allocating a state machine when we don't
|
// Perf: separated to avoid allocating a state machine when we don't
|
||||||
// need to go async.
|
// need to go async.
|
||||||
return BindModelCoreAsync(bindingContext);
|
return BindModelCoreAsync(bindingContext);
|
||||||
|
|
@ -345,13 +323,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
// application developer should know that this was an invalid type to try to bind to.
|
// application developer should know that this was an invalid type to try to bind to.
|
||||||
if (_modelCreator == null)
|
if (_modelCreator == null)
|
||||||
{
|
{
|
||||||
|
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
|
||||||
|
// reflection does not provide information about the implicit parameterless constructor for a struct.
|
||||||
|
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
|
||||||
|
// compile fails to construct it.
|
||||||
|
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
|
||||||
|
if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
|
||||||
|
{
|
||||||
|
if (bindingContext.IsTopLevelObject)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_TopLevelObject(modelTypeInfo.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty(
|
||||||
|
modelTypeInfo.FullName,
|
||||||
|
bindingContext.ModelName,
|
||||||
|
bindingContext.ModelMetadata.ContainerType.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
_modelCreator = Expression
|
_modelCreator = Expression
|
||||||
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
|
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
|
||||||
.Compile();
|
.Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _modelCreator();
|
return _modelCreator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -349,6 +348,52 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.IsType<Person>(model);
|
Assert.IsType<Person>(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateModel_ForStructModelType_AsTopLevelObject_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new DefaultModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = GetMetadataForType(typeof(PointStruct)),
|
||||||
|
IsTopLevelObject = true
|
||||||
|
};
|
||||||
|
var binder = CreateBinder(bindingContext.ModelMetadata);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => binder.CreateModelPublic(bindingContext));
|
||||||
|
Assert.Equal(
|
||||||
|
string.Format(
|
||||||
|
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
|
||||||
|
"value types and must have a parameterless constructor.",
|
||||||
|
typeof(PointStruct).FullName),
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateModel_ForStructModelType_AsProperty_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new DefaultModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = GetMetadataForProperty(typeof(Location), nameof(Location.Point)),
|
||||||
|
ModelName = nameof(Location.Point),
|
||||||
|
IsTopLevelObject = false
|
||||||
|
};
|
||||||
|
var binder = CreateBinder(bindingContext.ModelMetadata);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => binder.CreateModelPublic(bindingContext));
|
||||||
|
Assert.Equal(
|
||||||
|
string.Format(
|
||||||
|
"Could not create an instance of type '{0}'. Model bound complex types must not be abstract or " +
|
||||||
|
"value types and must have a parameterless constructor. Alternatively, set the '{1}' property to" +
|
||||||
|
" a non-null value in the '{2}' constructor.",
|
||||||
|
typeof(PointStruct).FullName,
|
||||||
|
nameof(Location.Point),
|
||||||
|
typeof(Location).FullName),
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindModelAsync_ModelIsNotNull_DoesNotCallCreateModel()
|
public async Task BindModelAsync_ModelIsNotNull_DoesNotCallCreateModel()
|
||||||
{
|
{
|
||||||
|
|
@ -1036,6 +1081,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return _metadataProvider.GetMetadataForProperty(type, propertyName);
|
return _metadataProvider.GetMetadataForProperty(type, propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class Location
|
||||||
|
{
|
||||||
|
public PointStruct Point { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PointStruct
|
||||||
|
{
|
||||||
|
public PointStruct(double x, double y)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
}
|
||||||
|
public double X { get; }
|
||||||
|
public double Y { get; }
|
||||||
|
}
|
||||||
|
|
||||||
private class BindingOptionalProperty
|
private class BindingOptionalProperty
|
||||||
{
|
{
|
||||||
[BindingBehavior(BindingBehavior.Optional)]
|
[BindingBehavior(BindingBehavior.Optional)]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
|
|
@ -460,6 +461,34 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
exception.Message);
|
exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionParameter_CustomModelBinder_CanCreateModels_ForParameterlessConstructorTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(binderProvider: new CustomComplexTypeModelBinderProvider());
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "prefix",
|
||||||
|
ParameterType = typeof(ClassWithNoDefaultConstructor)
|
||||||
|
};
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
// Model
|
||||||
|
Assert.NotNull(modelBindingResult.Model);
|
||||||
|
var boundModel = Assert.IsType<ClassWithNoDefaultConstructor>(modelBindingResult.Model);
|
||||||
|
Assert.Equal(100, boundModel.Id);
|
||||||
|
|
||||||
|
// ModelState
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
private struct PointStruct
|
private struct PointStruct
|
||||||
{
|
{
|
||||||
public PointStruct(double x, double y)
|
public PointStruct(double x, double y)
|
||||||
|
|
@ -479,8 +508,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
|
|
||||||
private class ClassWithNoDefaultConstructor
|
private class ClassWithNoDefaultConstructor
|
||||||
{
|
{
|
||||||
public ClassWithNoDefaultConstructor(int id) { }
|
public ClassWithNoDefaultConstructor(int id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
public string City { get; set; }
|
public string City { get; set; }
|
||||||
|
public int Id { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class AbstractClassWithNoDefaultConstructor
|
private abstract class AbstractClassWithNoDefaultConstructor
|
||||||
|
|
@ -562,5 +595,34 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
return GetEnumerator();
|
return GetEnumerator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// By default the ComplexTypeModelBinder fails to construct models for types with no parameterless constructor,
|
||||||
|
// but a developer could change this behavior by overridng CreateModel
|
||||||
|
private class CustomComplexTypeModelBinder : ComplexTypeModelBinder
|
||||||
|
{
|
||||||
|
public CustomComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
|
||||||
|
: base(propertyBinders)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override object CreateModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(ClassWithNoDefaultConstructor), bindingContext.ModelType);
|
||||||
|
return new ClassWithNoDefaultConstructor(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomComplexTypeModelBinderProvider : IModelBinderProvider
|
||||||
|
{
|
||||||
|
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||||
|
{
|
||||||
|
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
|
||||||
|
foreach (var property in context.Metadata.Properties)
|
||||||
|
{
|
||||||
|
propertyBinders.Add(property, context.CreateBinder(property));
|
||||||
|
}
|
||||||
|
return new CustomComplexTypeModelBinder(propertyBinders);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue