diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
index 782b84b2cd..af97d9a043 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
@@ -66,17 +66,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
};
}
+ ///
+ /// Creates a for the provided parameter.
+ ///
+ /// The .
+ /// A .
public static ModelMetadataIdentity ForParameter(ParameterInfo parameter)
+ => ForParameter(parameter, parameter?.ParameterType);
+
+ ///
+ /// Creates a for the provided parameter with the specified
+ /// model type.
+ ///
+ /// The .
+ /// The model type.
+ /// A .
+ public static ModelMetadataIdentity ForParameter(ParameterInfo parameter, Type modelType)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
return new ModelMetadataIdentity()
{
Name = parameter.Name,
- ModelType = parameter.ParameterType,
+ ModelType = modelType,
ParameterInfo = parameter,
};
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
index 1b4f01cd6d..147ccc45f5 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
@@ -32,5 +32,27 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// The .
/// A instance describing the .
public abstract ModelMetadata GetMetadataForParameter(ParameterInfo parameter);
+
+ ///
+ /// Supplies metadata describing a parameter.
+ ///
+ /// The
+ /// The actual model type.
+ /// A instance describing the .
+ public virtual ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Supplies metadata describing a property.
+ ///
+ /// The .
+ /// The actual model type.
+ /// A instance describing the .
+ public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType)
+ {
+ throw new NotSupportedException();
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
index 7c41dda166..709dffcf5c 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
@@ -3,16 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Controllers
{
///
/// A descriptor for model bound properties of a controller.
///
- public class ControllerBoundPropertyDescriptor : ParameterDescriptor
+ public class ControllerBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor
{
///
- /// Gets or sets the for this property.
+ /// Gets or sets the for this property.
///
public PropertyInfo PropertyInfo { get; set; }
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
index e48ef9dd00..593a40d0fc 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
@@ -3,16 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Controllers
{
///
/// A descriptor for method parameters of an action method.
///
- public class ControllerParameterDescriptor : ParameterDescriptor
+ public class ControllerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor
{
///
- /// Gets or sets the .
+ /// Gets or sets the .
///
public ParameterInfo ParameterInfo { get; set; }
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs
new file mode 100644
index 0000000000..93e6a09b28
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ ///
+ /// A for action parameters.
+ ///
+ public interface IParameterInfoParameterDescriptor
+ {
+ ///
+ /// Gets the .
+ ///
+ ParameterInfo ParameterInfo { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs
new file mode 100644
index 0000000000..5a9a8682d1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ ///
+ /// A for bound properties.
+ ///
+ public interface IPropertyInfoParameterDescriptor
+ {
+ ///
+ /// Gets the .
+ ///
+ PropertyInfo PropertyInfo { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
index 5b79143002..b9e374701e 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
@@ -97,14 +98,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Details.Properties;
}
+ ///
public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter)
+ => GetMetadataForParameter(parameter, parameter?.ParameterType);
+
+ ///
+ public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
- var cacheEntry = GetCacheEntry(parameter);
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ var cacheEntry = GetCacheEntry(parameter, modelType);
return cacheEntry.Metadata;
}
@@ -122,6 +133,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Metadata;
}
+ ///
+ public override ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType)
+ {
+ if (propertyInfo == null)
+ {
+ throw new ArgumentNullException(nameof(propertyInfo));
+ }
+
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ var cacheEntry = GetCacheEntry(propertyInfo, modelType);
+
+ return cacheEntry.Metadata;
+ }
+
private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions optionsAccessor)
{
if (optionsAccessor == null)
@@ -151,10 +180,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry;
}
- private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter)
+ private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType)
{
return _typeCache.GetOrAdd(
- ModelMetadataIdentity.ForParameter(parameter),
+ ModelMetadataIdentity.ForParameter(parameter, modelType),
+ _cacheEntryFactory);
+ }
+
+ private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType)
+ {
+ return _typeCache.GetOrAdd(
+ ModelMetadataIdentity.ForProperty(modelType, property.Name, property.DeclaringType),
_cacheEntryFactory);
}
@@ -165,6 +201,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
details = CreateParameterDetails(key);
}
+ else if (key.MetadataKind == ModelMetadataKind.Property)
+ {
+ details = CreateSinglePropertyDetails(key);
+ }
else
{
details = CreateTypeDetails(key);
@@ -174,6 +214,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return new ModelMetadataCacheEntry(metadata, details);
}
+ private DefaultMetadataDetails CreateSinglePropertyDetails(ModelMetadataIdentity propertyKey)
+ {
+ var propertyHelpers = PropertyHelper.GetVisibleProperties(propertyKey.ContainerType);
+ for (var i = 0; i < propertyHelpers.Length; i++)
+ {
+ var propertyHelper = propertyHelpers[i];
+ if (propertyHelper.Name == propertyKey.Name)
+ {
+ return CreateSinglePropertyDetails(propertyKey, propertyHelper);
+ }
+ }
+
+ Debug.Fail($"Unable to find property '{propertyKey.Name}' on type '{propertyKey.ContainerType}.");
+ return null;
+ }
+
private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType()
{
var key = ModelMetadataIdentity.ForType(typeof(object));
@@ -217,35 +273,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
for (var i = 0; i < propertyHelpers.Length; i++)
{
var propertyHelper = propertyHelpers[i];
+
var propertyKey = ModelMetadataIdentity.ForProperty(
propertyHelper.Property.PropertyType,
propertyHelper.Name,
key.ModelType);
- var attributes = ModelAttributes.GetAttributesForProperty(
- key.ModelType,
- propertyHelper.Property);
-
- var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes);
- if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true)
- {
- var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property);
- propertyEntry.PropertyGetter = getter;
- }
-
- if (propertyHelper.Property.CanWrite &&
- propertyHelper.Property.SetMethod?.IsPublic == true &&
- !key.ModelType.GetTypeInfo().IsValueType)
- {
- propertyEntry.PropertySetter = propertyHelper.ValueSetter;
- }
-
+ var propertyEntry = CreateSinglePropertyDetails(propertyKey, propertyHelper);
propertyEntries.Add(propertyEntry);
}
return propertyEntries.ToArray();
}
+ private DefaultMetadataDetails CreateSinglePropertyDetails(
+ ModelMetadataIdentity propertyKey,
+ PropertyHelper propertyHelper)
+ {
+ Debug.Assert(propertyKey.MetadataKind == ModelMetadataKind.Property);
+ var containerType = propertyKey.ContainerType;
+
+ var attributes = ModelAttributes.GetAttributesForProperty(
+ containerType,
+ propertyHelper.Property,
+ propertyKey.ModelType);
+
+ var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes);
+ if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true)
+ {
+ var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property);
+ propertyEntry.PropertyGetter = getter;
+ }
+
+ if (propertyHelper.Property.CanWrite &&
+ propertyHelper.Property.SetMethod?.IsPublic == true &&
+ !containerType.IsValueType)
+ {
+ propertyEntry.PropertySetter = propertyHelper.ValueSetter;
+ }
+
+ return propertyEntry;
+ }
+
///
/// Creates the entry for a model .
///
@@ -269,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
return new DefaultMetadataDetails(
key,
- ModelAttributes.GetAttributesForParameter(key.ParameterInfo));
+ ModelAttributes.GetAttributesForParameter(key.ParameterInfo, key.ModelType));
}
private class TypeCache : ConcurrentDictionary
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
index e5b59933ae..0f9a5bf103 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
@@ -135,9 +135,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
///
public static ModelAttributes GetAttributesForProperty(Type type, PropertyInfo property)
{
- if (type == null)
+ return GetAttributesForProperty(type, property, property.PropertyType);
+ }
+
+ ///
+ /// Gets the attributes for the given with the specified .
+ ///
+ /// The in which caller found .
+ ///
+ /// A for which attributes need to be resolved.
+ ///
+ /// The model type
+ ///
+ /// A instance with the attributes of the property and its .
+ ///
+ public static ModelAttributes GetAttributesForProperty(Type containerType, PropertyInfo property, Type modelType)
+ {
+ if (containerType == null)
{
- throw new ArgumentNullException(nameof(type));
+ throw new ArgumentNullException(nameof(containerType));
}
if (property == null)
@@ -146,9 +162,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
var propertyAttributes = property.GetCustomAttributes();
- var typeAttributes = property.PropertyType.GetTypeInfo().GetCustomAttributes();
+ var typeAttributes = modelType.GetCustomAttributes();
- var metadataType = GetMetadataType(type);
+ var metadataType = GetMetadataType(containerType);
if (metadataType != null)
{
var metadataProperty = metadataType.GetRuntimeProperty(property.Name);
@@ -174,12 +190,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(type));
}
- var attributes = type.GetTypeInfo().GetCustomAttributes();
+ var attributes = type.GetCustomAttributes();
var metadataType = GetMetadataType(type);
if (metadataType != null)
{
- attributes = attributes.Concat(metadataType.GetTypeInfo().GetCustomAttributes());
+ attributes = attributes.Concat(metadataType.GetCustomAttributes());
}
return new ModelAttributes(attributes, propertyAttributes: null, parameterAttributes: null);
@@ -205,9 +221,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes);
}
+ ///
+ /// Gets the attributes for the given with the specified .
+ ///
+ ///
+ /// The for which attributes need to be resolved.
+ ///
+ /// The model type.
+ ///
+ /// A instance with the attributes of the parameter and its .
+ ///
+ public static ModelAttributes GetAttributesForParameter(ParameterInfo parameterInfo, Type modelType)
+ {
+ if (parameterInfo == null)
+ {
+ throw new ArgumentNullException(nameof(parameterInfo));
+ }
+
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ // Prior versions called IModelMetadataProvider.GetMetadataForType(...) and therefore
+ // GetAttributesForType(...) for parameters. Maintain that set of attributes (including those from an
+ // ModelMetadataTypeAttribute reference) for back-compatibility.
+ var typeAttributes = GetAttributesForType(modelType).TypeAttributes;
+ var parameterAttributes = parameterInfo.GetCustomAttributes();
+
+ return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes);
+ }
+
private static Type GetMetadataType(Type type)
{
- return type.GetTypeInfo().GetCustomAttribute()?.MetadataType;
+ return type.GetCustomAttribute()?.MetadataType;
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
index 90a2458f2a..a7f1b8e5ba 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
@@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
@@ -282,6 +283,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelBindingContext modelBindingContext,
ModelBindingResult modelBindingResult)
{
+ RecalculateModelMetadata(parameter, modelBindingResult, ref metadata);
+
if (!modelBindingResult.IsModelSet && metadata.IsBindingRequired)
{
// Enforce BindingBehavior.Required (e.g., [BindRequired])
@@ -329,5 +332,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
metadata);
}
}
+
+ private void RecalculateModelMetadata(
+ ParameterDescriptor parameter,
+ ModelBindingResult modelBindingResult,
+ ref ModelMetadata metadata)
+ {
+ // Attempt to recalculate ModelMetadata for top level parameters and properties using the actual
+ // model type. This ensures validation uses a combination of top-level validation metadata
+ // as well as metadata on the actual, rather than declared, model type.
+
+ if (!modelBindingResult.IsModelSet ||
+ modelBindingResult.Model == null ||
+ !(_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider))
+ {
+ return;
+ }
+
+ var modelType = modelBindingResult.Model.GetType();
+ if (parameter is IParameterInfoParameterDescriptor parameterInfoParameter)
+ {
+ var parameterInfo = parameterInfoParameter.ParameterInfo;
+ if (modelType != parameterInfo.ParameterType)
+ {
+ metadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo, modelType);
+ }
+ }
+ else if (parameter is IPropertyInfoParameterDescriptor propertyInfoParameter)
+ {
+ var propertyInfo = propertyInfoParameter.PropertyInfo;
+ if (modelType != propertyInfo.PropertyType)
+ {
+ metadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, modelType);
+ }
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
index dcf212e077..099b86192c 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
@@ -3,11 +3,15 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
- public class HandlerParameterDescriptor : ParameterDescriptor
+ public class HandlerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor
{
+ ///
+ /// Gets or sets the .
+ ///
public ParameterInfo ParameterInfo { get; set; }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
index 56cbdc40f7..cb02222300 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
@@ -3,11 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
- public class PageBoundPropertyDescriptor : ParameterDescriptor
+ public class PageBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor
{
+ ///
+ /// Gets or sets the for this property.
+ ///
public PropertyInfo Property { get; set; }
+
+ PropertyInfo IPropertyInfoParameterDescriptor.PropertyInfo => Property;
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs
index 4286f263c9..8994835817 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Reflection;
using Microsoft.Extensions.Options;
using Xunit;
@@ -263,6 +264,173 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
Assert.Same(metadata1, metadata2);
}
+ [Fact]
+ public void GetMetadataForParameter_WithModelType_ReturnsCombinedModelMetadata()
+ {
+ // Arrange
+ var parameter = GetType()
+ .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
+ .GetParameters()[0];
+ var provider = CreateProvider();
+
+ // Act
+ var metadata = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
+
+ // Assert
+ Assert.Equal(ModelMetadataKind.Parameter, metadata.MetadataKind);
+ Assert.Equal(typeof(DerivedModelType), metadata.ModelType);
+
+ var defaultModelMetadata = Assert.IsType(metadata);
+
+ Assert.Collection(
+ defaultModelMetadata.Attributes.Attributes,
+ a => Assert.Equal("OnParameter", Assert.IsType(a).Value),
+ a => Assert.Equal("OnDerivedType", Assert.IsType(a).Value),
+ a => Assert.Equal("OnType", Assert.IsType(a).Value));
+
+ Assert.Collection(
+ metadata.Properties.OrderBy(p => p.Name),
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
+
+ var defaultPropertyMetadata = Assert.IsType(p);
+ Assert.Collection(
+ defaultPropertyMetadata.Attributes.Attributes.OfType(),
+ a => Assert.Equal("OnDerivedProperty", Assert.IsType(a).Value));
+ },
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
+
+ var defaultPropertyMetadata = Assert.IsType(p);
+ Assert.Collection(
+ defaultPropertyMetadata.Attributes.Attributes.OfType(),
+ a => Assert.Equal("OnProperty", Assert.IsType(a).Value),
+ a => Assert.Equal("OnPropertyType", Assert.IsType(a).Value));
+ },
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.Property2), p.Name);
+ });
+ }
+
+ [Fact]
+ public void GetMetadataForParameter_WithModelType_CachesResults()
+ {
+ // Arrange
+ var parameter = GetType()
+ .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
+ .GetParameters()[0];
+ var provider = CreateProvider();
+
+ // Act
+ var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
+ var metadata2 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
+
+ // Assert
+ Assert.Same(metadata1, metadata2);
+ }
+
+ [Fact]
+ public void GetMetadataForParameter_WithModelType_VariesByModelType()
+ {
+ // Arrange
+ var parameter = GetType()
+ .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
+ .GetParameters()[0];
+ var provider = CreateProvider();
+
+ // Act
+ var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
+ var metadata2 = provider.GetMetadataForParameter(parameter, typeof(object));
+
+ // Assert
+ Assert.NotSame(metadata1, metadata2);
+ }
+
+ [Fact]
+ public void GetMetadataForProperty_WithModelType_ReturnsCombinedModelMetadata()
+ {
+ // Arrange
+ var property = typeof(TestContainer)
+ .GetProperty(nameof(TestContainer.ModelProperty));
+ var provider = CreateProvider();
+
+ // Act
+ var metadata = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
+
+ // Assert
+ Assert.Equal(ModelMetadataKind.Property, metadata.MetadataKind);
+ Assert.Equal(typeof(DerivedModelType), metadata.ModelType);
+
+ var defaultModelMetadata = Assert.IsType(metadata);
+
+ Assert.Collection(
+ defaultModelMetadata.Attributes.Attributes,
+ a => Assert.Equal("OnProperty", Assert.IsType(a).Value),
+ a => Assert.Equal("OnDerivedType", Assert.IsType(a).Value),
+ a => Assert.Equal("OnType", Assert.IsType(a).Value));
+
+ Assert.Collection(
+ metadata.Properties.OrderBy(p => p.Name),
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
+
+ var defaultPropertyMetadata = Assert.IsType(p);
+ Assert.Collection(
+ defaultPropertyMetadata.Attributes.Attributes.OfType(),
+ a => Assert.Equal("OnDerivedProperty", Assert.IsType(a).Value));
+ },
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
+
+ var defaultPropertyMetadata = Assert.IsType(p);
+ Assert.Collection(
+ defaultPropertyMetadata.Attributes.Attributes.OfType(),
+ a => Assert.Equal("OnProperty", Assert.IsType(a).Value),
+ a => Assert.Equal("OnPropertyType", Assert.IsType(a).Value));
+ },
+ p =>
+ {
+ Assert.Equal(nameof(DerivedModelType.Property2), p.Name);
+ });
+ }
+
+ [Fact]
+ public void GetMetadataForProperty_WithModelType_CachesResults()
+ {
+ // Arrange
+ var property = typeof(TestContainer)
+ .GetProperty(nameof(TestContainer.ModelProperty));
+ var provider = CreateProvider();
+
+ // Act
+ var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
+ var metadata2 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
+
+ // Assert
+ Assert.Same(metadata1, metadata2);
+ }
+
+ [Fact]
+ public void GetMetadataForProperty_WithModelType_VariesByModelType()
+ {
+ // Arrange
+ var property = typeof(TestContainer)
+ .GetProperty(nameof(TestContainer.ModelProperty));
+ var provider = CreateProvider();
+
+ // Act
+ var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
+ var metadata2 = provider.GetMetadataForProperty(property, typeof(object));
+
+ // Assert
+ Assert.NotSame(metadata1, metadata2);
+ }
+
private static DefaultModelMetadataProvider CreateProvider()
{
return new DefaultModelMetadataProvider(
@@ -321,5 +489,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
public new string Property { get; set; }
}
+
+ [Model("OnDerivedType")]
+ private class DerivedModelType : ModelType
+ {
+ [Model("OnDerivedProperty")]
+ public string DerivedProperty { get; set; }
+ }
+
+ private class TestContainer
+ {
+ [Model("OnProperty")]
+ public ModelType ModelProperty { get; set; }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs
index 3b0b3b0d92..3174e0110d 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs
@@ -5,7 +5,6 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
-using System.Runtime.CompilerServices;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -242,6 +241,55 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.IsType(Assert.Single(attributes.TypeAttributes));
}
+ [Fact]
+ public void GetAttributesForParameter_WithModelType_IncludesTypeAttributes()
+ {
+ // Arrange
+ var parameters = typeof(MethodWithParamAttributesType)
+ .GetMethod(nameof(MethodWithParamAttributesType.Method))
+ .GetParameters();
+
+ // Act
+ var attributes = ModelAttributes.GetAttributesForParameter(parameters[2], typeof(DerivedModelWithAttributes));
+
+ // Assert
+ Assert.Collection(
+ attributes.Attributes,
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute));
+ Assert.IsType(Assert.Single(attributes.ParameterAttributes));
+ Assert.Null(attributes.PropertyAttributes);
+ Assert.Collection(
+ attributes.TypeAttributes,
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute));
+ }
+
+ [Fact]
+ public void GetAttributesForProperty_WithModelType_IncludesTypeAttributes()
+ {
+ // Arrange
+ var property = typeof(MergedAttributes)
+ .GetProperty(nameof(MergedAttributes.BaseModel));
+
+ // Act
+ var attributes = ModelAttributes.GetAttributesForProperty(typeof(MergedAttributes), property, typeof(DerivedModelWithAttributes));
+
+ // Assert
+ Assert.Collection(
+ attributes.Attributes,
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute));
+ Assert.IsType(Assert.Single(attributes.PropertyAttributes));
+ Assert.Null(attributes.ParameterAttributes);
+ Assert.Collection(
+ attributes.TypeAttributes,
+ attribute => Assert.IsType(attribute),
+ attribute => Assert.IsType(attribute));
+ }
+
[ClassValidator]
private class BaseModel
{
@@ -272,6 +320,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
+ [ModelBinder(Name = "Custom")]
+ private class DerivedModelWithAttributes : BaseModel
+ {
+ }
+
[ModelMetadataType(typeof(BaseModel))]
private class BaseViewModel
{
@@ -313,6 +366,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
[Required]
public PropertyType Property { get; set; }
+
+ [BindRequired]
+ public BaseModel BaseModel { get; set; }
}
private class MergedAttributesMetadata
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs
index 6a49060f19..7a605c4640 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -491,6 +492,220 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
}
+ [Fact]
+ public async Task BindModelAsync_ForParameter_UsesValidationFromActualModel_WhenDerivedModelIsSet()
+ {
+ // Arrange
+ var method = GetType().GetMethod(nameof(TestMethodWithoutAttributes), BindingFlags.NonPublic | BindingFlags.Instance);
+ var parameter = method.GetParameters()[0];
+ var parameterDescriptor = new ControllerParameterDescriptor
+ {
+ ParameterInfo = parameter,
+ Name = parameter.Name,
+ };
+
+ var actionContext = GetControllerContext();
+ var modelMetadataProvider = new TestModelMetadataProvider();
+
+ var model = new DerivedPerson();
+ var modelBindingResult = ModelBindingResult.Success(model);
+
+ var parameterBinder = new ParameterBinder(
+ modelMetadataProvider,
+ Mock.Of(),
+ new DefaultObjectValidator(
+ modelMetadataProvider,
+ new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
+ _optionsAccessor,
+ NullLoggerFactory.Instance);
+
+ var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter);
+ var modelBinder = CreateMockModelBinder(modelBindingResult);
+
+ // Act
+ var result = await parameterBinder.BindModelAsync(
+ actionContext,
+ modelBinder,
+ CreateMockValueProvider(),
+ parameterDescriptor,
+ modelMetadata,
+ value: null);
+
+ // Assert
+ Assert.True(result.IsModelSet);
+ Assert.Same(model, result.Model);
+
+ Assert.False(actionContext.ModelState.IsValid);
+ Assert.Collection(
+ actionContext.ModelState,
+ kvp =>
+ {
+ Assert.Equal($"{parameter.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key);
+ var error = Assert.Single(kvp.Value.Errors);
+ Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage);
+ });
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ForParameter_UsesValidationFromParameter_WhenDerivedModelIsSet()
+ {
+ // Arrange
+ var method = GetType().GetMethod(nameof(TestMethodWithAttributes), BindingFlags.NonPublic | BindingFlags.Instance);
+ var parameter = method.GetParameters()[0];
+ var parameterDescriptor = new ControllerParameterDescriptor
+ {
+ ParameterInfo = parameter,
+ Name = parameter.Name,
+ };
+
+ var actionContext = GetControllerContext();
+ var modelMetadataProvider = new TestModelMetadataProvider();
+
+ var model = new DerivedPerson { DerivedProperty = "SomeValue" };
+ var modelBindingResult = ModelBindingResult.Success(model);
+
+ var parameterBinder = new ParameterBinder(
+ modelMetadataProvider,
+ Mock.Of(),
+ new DefaultObjectValidator(
+ modelMetadataProvider,
+ new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
+ _optionsAccessor,
+ NullLoggerFactory.Instance);
+
+ var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter);
+ var modelBinder = CreateMockModelBinder(modelBindingResult);
+
+ // Act
+ var result = await parameterBinder.BindModelAsync(
+ actionContext,
+ modelBinder,
+ CreateMockValueProvider(),
+ parameterDescriptor,
+ modelMetadata,
+ value: null);
+
+ // Assert
+ Assert.True(result.IsModelSet);
+ Assert.Same(model, result.Model);
+
+ Assert.False(actionContext.ModelState.IsValid);
+ Assert.Collection(
+ actionContext.ModelState,
+ kvp =>
+ {
+ Assert.Equal(parameter.Name, kvp.Key);
+ var error = Assert.Single(kvp.Value.Errors);
+ Assert.Equal("Always Invalid", error.ErrorMessage);
+ });
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ForProperty_UsesValidationFromActualModel_WhenDerivedModelIsSet()
+ {
+ // Arrange
+ var property = typeof(TestController).GetProperty(nameof(TestController.Model));
+ var parameterDescriptor = new ControllerBoundPropertyDescriptor
+ {
+ PropertyInfo = property,
+ Name = property.Name,
+ };
+
+ var actionContext = GetControllerContext();
+ var modelMetadataProvider = new TestModelMetadataProvider();
+
+ var model = new DerivedModel();
+ var modelBindingResult = ModelBindingResult.Success(model);
+
+ var parameterBinder = new ParameterBinder(
+ modelMetadataProvider,
+ Mock.Of(),
+ new DefaultObjectValidator(
+ modelMetadataProvider,
+ new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
+ _optionsAccessor,
+ NullLoggerFactory.Instance);
+
+ var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
+ var modelBinder = CreateMockModelBinder(modelBindingResult);
+
+ // Act
+ var result = await parameterBinder.BindModelAsync(
+ actionContext,
+ modelBinder,
+ CreateMockValueProvider(),
+ parameterDescriptor,
+ modelMetadata,
+ value: null);
+
+ // Assert
+ Assert.True(result.IsModelSet);
+ Assert.Same(model, result.Model);
+
+ Assert.False(actionContext.ModelState.IsValid);
+ Assert.Collection(
+ actionContext.ModelState,
+ kvp =>
+ {
+ Assert.Equal($"{property.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key);
+ var error = Assert.Single(kvp.Value.Errors);
+ Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage);
+ });
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ForProperty_UsesValidationOnProperty_WhenDerivedModelIsSet()
+ {
+ // Arrange
+ var property = typeof(TestControllerWithValidatedProperties).GetProperty(nameof(TestControllerWithValidatedProperties.Model));
+ var parameterDescriptor = new ControllerBoundPropertyDescriptor
+ {
+ PropertyInfo = property,
+ Name = property.Name,
+ };
+
+ var actionContext = GetControllerContext();
+ var modelMetadataProvider = new TestModelMetadataProvider();
+
+ var model = new DerivedModel { DerivedProperty = "some value" };
+ var modelBindingResult = ModelBindingResult.Success(model);
+
+ var parameterBinder = new ParameterBinder(
+ modelMetadataProvider,
+ Mock.Of(),
+ new DefaultObjectValidator(
+ modelMetadataProvider,
+ new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
+ _optionsAccessor,
+ NullLoggerFactory.Instance);
+
+ var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
+ var modelBinder = CreateMockModelBinder(modelBindingResult);
+
+ // Act
+ var result = await parameterBinder.BindModelAsync(
+ actionContext,
+ modelBinder,
+ CreateMockValueProvider(),
+ parameterDescriptor,
+ modelMetadata,
+ value: null);
+
+ // Assert
+ Assert.True(result.IsModelSet);
+ Assert.Same(model, result.Model);
+
+ Assert.False(actionContext.ModelState.IsValid);
+ Assert.Collection(
+ actionContext.ModelState,
+ kvp =>
+ {
+ Assert.Equal($"{property.Name}", kvp.Key);
+ var error = Assert.Single(kvp.Value.Errors);
+ Assert.Equal("Always Invalid", error.ErrorMessage);
+ });
+ }
+
private static ControllerContext GetControllerContext()
{
var services = new ServiceCollection();
@@ -641,6 +856,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public IList Kids { get; } = new List();
}
+ private class DerivedPerson : Person
+ {
+ [Required]
+ public string DerivedProperty { get; set; }
+ }
+
+ [Required]
+ private Person PersonProperty { get; set; }
+
public abstract class FakeModelMetadata : ModelMetadata
{
public FakeModelMetadata()
@@ -648,5 +872,44 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
}
}
+
+ private void TestMethodWithoutAttributes(Person person) { }
+
+ private void TestMethodWithAttributes([Required][AlwaysInvalid] Person person) { }
+
+ private class TestController
+ {
+ public BaseModel Model { get; set; }
+ }
+
+ private class TestControllerWithValidatedProperties
+ {
+ [AlwaysInvalid]
+ [Required]
+ public BaseModel Model { get; set; }
+ }
+
+ private class BaseModel
+ {
+ }
+
+ private class DerivedModel
+ {
+ [Required]
+ public string DerivedProperty { get; set; }
+ }
+
+ private class AlwaysInvalidAttribute : ValidationAttribute
+ {
+ public AlwaysInvalidAttribute()
+ {
+ ErrorMessage = "Always Invalid";
+ }
+
+ public override bool IsValid(object value)
+ {
+ return false;
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
index c93092b339..6761579f04 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs
@@ -3,15 +3,14 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
-using System.Text;
using System.Threading.Tasks;
using BasicWebSite.Models;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs
index 590e1d02a9..fe8d4f300b 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs
@@ -7,7 +7,6 @@ using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
-using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@@ -32,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public static async Task AssertStatusCodeAsync(this HttpResponseMessage response, HttpStatusCode expectedStatusCode)
{
- if (response.StatusCode == HttpStatusCode.OK)
+ if (response.StatusCode == expectedStatusCode)
{
return response;
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
index 2907b248c8..2425c21f3a 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
@@ -3,11 +3,12 @@
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
+using FormatterWebSite.Models;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@@ -217,5 +218,100 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
}
+
+ [Fact]
+ public async Task BindingWorksForPolymorphicTypes()
+ {
+ // Act
+ var response = await Client.GetAsync("PolymorphicBinding/ModelBound?DerivedProperty=Test");
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+ var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ Assert.Equal("Test", result.DerivedProperty);
+ }
+
+ [Fact]
+ public async Task ValidationUsesModelMetadataFromActualModelType_ForModelBoundParameters()
+ {
+ // Act
+ var response = await Client.GetAsync("PolymorphicBinding/ModelBound");
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+ Assert.Collection(
+ result.Properties(),
+ p =>
+ {
+ Assert.Equal("DerivedProperty", p.Name);
+ var value = Assert.IsType(p.Value);
+ Assert.Equal("The DerivedProperty field is required.", value.First);
+ });
+ }
+
+ [Fact]
+ public async Task InputFormatterWorksForPolymorphicTypes()
+ {
+ // Act
+ var input = "Test";
+ var response = await Client.PostAsJsonAsync("PolymorphicBinding/InputFormatted", input);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+ var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ Assert.Equal(input, result.DerivedProperty);
+ }
+
+ [Fact]
+ public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormattedParameters()
+ {
+ // Act
+ var response = await Client.PostAsJsonAsync("PolymorphicBinding/InputFormatted", string.Empty);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+ Assert.Collection(
+ result.Properties(),
+ p =>
+ {
+ Assert.Equal("DerivedProperty", p.Name);
+ var value = Assert.IsType(p.Value);
+ Assert.Equal("The DerivedProperty field is required.", value.First);
+ });
+ }
+
+ [Fact]
+ public async Task InputFormatterWorksForPolymorphicProperties()
+ {
+ // Act
+ var input = "Test";
+ var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", input);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+ var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ Assert.Equal(input, result.DerivedProperty);
+ }
+
+ [Fact]
+ public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormattedProperties()
+ {
+ // Act
+ var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", string.Empty);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+ Assert.Collection(
+ result.Properties(),
+ p =>
+ {
+ Assert.Equal("DerivedProperty", p.Name);
+ var value = Assert.IsType(p.Value);
+ Assert.Equal("The DerivedProperty field is required.", value.First);
+ });
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
index 524f8bd977..d97e128727 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
@@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Testing;
+using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@@ -738,6 +739,65 @@ Hello from /Pages/WithViewStart/Index.cshtml!";
}
}
+ [Fact]
+ public async Task PolymorphicPropertiesOnPageModelsAreBound()
+ {
+ // Arrange
+ var name = "TestName";
+ var age = 23;
+ var expected = $"Name = {name}, Age = {age}";
+ var request = new HttpRequestMessage(HttpMethod.Post, "Pages/PropertyBinding/PolymorphicBinding")
+ {
+ Content = new FormUrlEncodedContent(new Dictionary
+ {
+ { "Name", name },
+ { "Age", age.ToString() },
+ }),
+ };
+ await AddAntiforgeryHeaders(request);
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.OK);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(expected, content);
+ }
+
+ [Fact]
+ public async Task PolymorphicPropertiesOnPageModelsAreValidated()
+ {
+ // Arrange
+ var name = "TestName";
+ var age = 123;
+ var expected = $"Name = {name}, Age = {age}";
+ var request = new HttpRequestMessage(HttpMethod.Post, "Pages/PropertyBinding/PolymorphicBinding")
+ {
+ Content = new FormUrlEncodedContent(new Dictionary
+ {
+ { "Name", name },
+ { "Age", age.ToString() },
+ }),
+ };
+ await AddAntiforgeryHeaders(request);
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ var result = JObject.Parse(await response.Content.ReadAsStringAsync());
+ Assert.Collection(
+ result.Properties(),
+ p =>
+ {
+ Assert.Equal("Age", p.Name);
+ var value = Assert.IsType(p.Value);
+ Assert.Equal("The field Age must be between 0 and 99.", value.First.ToString());
+ });
+ }
+
[Fact]
public async Task HandlerMethodArgumentsAndPropertiesAreModelBound()
{
diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs
index 9d4db8b9da..9e20cb447c 100644
--- a/test/WebSites/BasicWebSite/Startup.cs
+++ b/test/WebSites/BasicWebSite/Startup.cs
@@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
-using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@@ -36,7 +35,7 @@ namespace BasicWebSite
var previous = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
- var result = (BadRequestObjectResult) previous(context);
+ var result = (BadRequestObjectResult)previous(context);
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
{
result.ContentTypes.Clear();
diff --git a/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs b/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs
new file mode 100644
index 0000000000..27a2643cfc
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs
@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using FormatterWebSite.Models;
+using Microsoft.AspNetCore.Mvc;
+
+namespace FormatterWebSite.Controllers
+{
+ public class PolymorhpicPropertyBindingController : ControllerBase
+ {
+ [FromBody]
+ public IModel Person { get; set; }
+
+ [HttpPost]
+ public IActionResult Action()
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ return Ok(Person);
+ }
+ }
+}
diff --git a/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs b/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs
new file mode 100644
index 0000000000..9358ec50d6
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs
@@ -0,0 +1,32 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using FormatterWebSite.Models;
+using Microsoft.AspNetCore.Mvc;
+
+namespace FormatterWebSite.Controllers
+{
+ public class PolymorphicBindingController : ControllerBase
+ {
+ public IActionResult ModelBound([ModelBinder(typeof(PolymorphicBinder))] BaseModel person)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ return Ok(person);
+ }
+
+ [HttpPost]
+ public IActionResult InputFormatted([FromBody] IModel person)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ return Ok(person);
+ }
+ }
+}
diff --git a/test/WebSites/FormatterWebSite/IModelConverter.cs b/test/WebSites/FormatterWebSite/IModelConverter.cs
new file mode 100644
index 0000000000..1ce4131f17
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/IModelConverter.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using FormatterWebSite.Models;
+using Newtonsoft.Json;
+
+namespace FormatterWebSite
+{
+ public class IModelConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(IModel);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ return new DerivedModel
+ {
+ DerivedProperty = reader.Value.ToString(),
+ };
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FormatterWebSite/Models/BaseModel.cs b/test/WebSites/FormatterWebSite/Models/BaseModel.cs
new file mode 100644
index 0000000000..b7b6567b1e
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Models/BaseModel.cs
@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace FormatterWebSite.Models
+{
+ public class BaseModel
+ {
+ public string BaseProperty { get; set; }
+ }
+}
diff --git a/test/WebSites/FormatterWebSite/Models/DerivedModel.cs b/test/WebSites/FormatterWebSite/Models/DerivedModel.cs
new file mode 100644
index 0000000000..2b10ac357a
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Models/DerivedModel.cs
@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.ComponentModel.DataAnnotations;
+
+namespace FormatterWebSite.Models
+{
+ public class DerivedModel : BaseModel, IModel
+ {
+ [Required]
+ [StringLength(10)]
+ public string DerivedProperty { get; set; }
+ }
+}
diff --git a/test/WebSites/FormatterWebSite/Models/IModel.cs b/test/WebSites/FormatterWebSite/Models/IModel.cs
new file mode 100644
index 0000000000..ce6e4b6d9b
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Models/IModel.cs
@@ -0,0 +1,9 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace FormatterWebSite.Models
+{
+ public interface IModel
+ {
+ }
+}
diff --git a/test/WebSites/FormatterWebSite/PolymorphicBinder.cs b/test/WebSites/FormatterWebSite/PolymorphicBinder.cs
new file mode 100644
index 0000000000..e7eefb71c2
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/PolymorphicBinder.cs
@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using FormatterWebSite.Models;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace FormatterWebSite.Controllers
+{
+ public class PolymorphicBinder : IModelBinder
+ {
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var model = new DerivedModel
+ {
+ DerivedProperty = bindingContext.ValueProvider.GetValue(nameof(DerivedModel.DerivedProperty)).FirstValue,
+ };
+
+ bindingContext.Result = ModelBindingResult.Success(model);
+
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs
index dd3beb9121..ee2744a028 100644
--- a/test/WebSites/FormatterWebSite/Startup.cs
+++ b/test/WebSites/FormatterWebSite/Startup.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
@@ -19,6 +20,8 @@ namespace FormatterWebSite
options.InputFormatters.Add(new StringInputFormatter());
})
.AddXmlDataContractSerializerFormatters();
+
+ services.Configure(options => { options.SerializerSettings.Converters.Insert(0, new IModelConverter()); });
}
diff --git a/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs b/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs
new file mode 100644
index 0000000000..19cc1d40a9
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs
@@ -0,0 +1,9 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace RazorPagesWebSite
+{
+ public interface IUserModel
+ {
+ }
+}
diff --git a/test/WebSites/RazorPagesWebSite/Models/UserModel.cs b/test/WebSites/RazorPagesWebSite/Models/UserModel.cs
index 62c2bf8cae..80c2d8fdc8 100644
--- a/test/WebSites/RazorPagesWebSite/Models/UserModel.cs
+++ b/test/WebSites/RazorPagesWebSite/Models/UserModel.cs
@@ -5,12 +5,17 @@ using System.ComponentModel.DataAnnotations;
namespace RazorPagesWebSite
{
- public class UserModel
+ public class UserModel : IUserModel
{
[Required]
public string Name { get; set; }
[Range(0, 99)]
public int Age { get; set; }
+
+ public override string ToString()
+ {
+ return $"Name = {Name}, Age = {Age}";
+ }
}
}
diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs
new file mode 100644
index 0000000000..0986986da0
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs
@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace RazorPagesWebSite
+{
+ public class PolymorphicBinding : PageModel
+ {
+ [ModelBinder(typeof(PolymorphicModelBinder))]
+ public IUserModel UserModel { get; set; }
+
+ public IActionResult OnPost()
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ return new ContentResult { Content = UserModel.ToString() };
+ }
+ }
+}
diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml
new file mode 100644
index 0000000000..7bde2525de
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml
@@ -0,0 +1,6 @@
+@page
+@model PolymorphicBinding
+
+
\ No newline at end of file
diff --git a/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs b/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs
new file mode 100644
index 0000000000..bce3d79b8a
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs
@@ -0,0 +1,32 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace RazorPagesWebSite
+{
+ public class PolymorphicModelBinder : IModelBinder
+ {
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var ageValue = bindingContext.ValueProvider.GetValue(nameof(UserModel.Age));
+ var age = 0;
+ if (ageValue.Length != 0)
+ {
+ age = int.Parse(ageValue.FirstValue);
+ }
+
+
+ var model = new UserModel
+ {
+ Name = bindingContext.ValueProvider.GetValue(nameof(UserModel.Name)).FirstValue,
+ Age = age,
+ };
+
+ bindingContext.Result = ModelBindingResult.Success(model);
+
+ return Task.CompletedTask;
+ }
+ }
+}