[Fixes #5801] Move call to validate constructor in ComplexTypeModelBinder into CreateModel

This commit is contained in:
Kiran Challa 2017-02-14 11:31:29 -08:00
parent 531c11df2a
commit 29647fda33
3 changed files with 153 additions and 33 deletions

View File

@ -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>

View File

@ -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)]

View File

@ -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);
}
}
}
}