[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; 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>

View File

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

View File

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