[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;
|
||||
}
|
||||
|
||||
// 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
|
||||
// need to go async.
|
||||
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.
|
||||
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
|
||||
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
|
||||
.Compile();
|
||||
}
|
||||
|
||||
return _modelCreator();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ 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;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
|
|
@ -349,6 +348,52 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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]
|
||||
public async Task BindModelAsync_ModelIsNotNull_DoesNotCallCreateModel()
|
||||
{
|
||||
|
|
@ -758,7 +803,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = metadata.Properties[nameof(model.PropertyWithDefaultValue)];
|
||||
|
||||
|
|
@ -780,7 +825,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = metadata.Properties[nameof(model.PropertyWithInitializedValue)];
|
||||
|
||||
|
|
@ -804,7 +849,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = metadata.Properties[nameof(model.PropertyWithInitializedValueAndDefault)];
|
||||
|
||||
|
|
@ -828,7 +873,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = metadata.Properties[nameof(model.NonUpdateableProperty)];
|
||||
|
||||
|
|
@ -917,7 +962,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
// Arrange
|
||||
var model = new Person();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfBirth)];
|
||||
|
||||
|
|
@ -943,7 +988,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
};
|
||||
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(Person));
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.DateOfDeath)];
|
||||
|
||||
|
|
@ -967,7 +1012,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
var model = new ModelWhosePropertySetterThrows();
|
||||
var bindingContext = CreateContext(GetMetadataForType(model.GetType()), model);
|
||||
bindingContext.ModelName = "foo";
|
||||
|
||||
|
||||
var metadata = GetMetadataForType(typeof(ModelWhosePropertySetterThrows));
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[nameof(model.NameNoAttribute)];
|
||||
|
||||
|
|
@ -1036,6 +1081,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
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
|
||||
{
|
||||
[BindingBehavior(BindingBehavior.Optional)]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||
|
|
@ -460,6 +461,34 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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
|
||||
{
|
||||
public PointStruct(double x, double y)
|
||||
|
|
@ -479,8 +508,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
private class ClassWithNoDefaultConstructor
|
||||
{
|
||||
public ClassWithNoDefaultConstructor(int id) { }
|
||||
public ClassWithNoDefaultConstructor(int id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
public string City { get; set; }
|
||||
public int Id { get; }
|
||||
}
|
||||
|
||||
private abstract class AbstractClassWithNoDefaultConstructor
|
||||
|
|
@ -562,5 +595,34 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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