Use ModelMetadata from actual types for validation
Fixes https://github.com/aspnet/Mvc/issues/7952
This commit is contained in:
parent
bd995d4cb1
commit
4f1b7ccca6
|
|
@ -66,17 +66,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ModelMetadataIdentity"/> for the provided parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The <see cref="ParameterInfo" />.</param>
|
||||
/// <returns>A <see cref="ModelMetadataIdentity"/>.</returns>
|
||||
public static ModelMetadataIdentity ForParameter(ParameterInfo parameter)
|
||||
=> ForParameter(parameter, parameter?.ParameterType);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ModelMetadataIdentity"/> for the provided parameter with the specified
|
||||
/// model type.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The <see cref="ParameterInfo" />.</param>
|
||||
/// <param name="modelType">The model type.</param>
|
||||
/// <returns>A <see cref="ModelMetadataIdentity"/>.</returns>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,5 +32,27 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// <param name="parameter">The <see cref="ParameterInfo"/>.</param>
|
||||
/// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="parameter"/>.</returns>
|
||||
public abstract ModelMetadata GetMetadataForParameter(ParameterInfo parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Supplies metadata describing a parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The <see cref="ParameterInfo"/></param>
|
||||
/// <param name="modelType">The actual model type.</param>
|
||||
/// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="parameter"/>.</returns>
|
||||
public virtual ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies metadata describing a property.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The <see cref="PropertyInfo"/>.</param>
|
||||
/// <param name="modelType">The actual model type.</param>
|
||||
/// <returns>A <see cref="ModelMetadata"/> instance describing the <paramref name="propertyInfo"/>.</returns>
|
||||
public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// A descriptor for model bound properties of a controller.
|
||||
/// </summary>
|
||||
public class ControllerBoundPropertyDescriptor : ParameterDescriptor
|
||||
public class ControllerBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="PropertyInfo"/> for this property.
|
||||
/// Gets or sets the <see cref="System.Reflection.PropertyInfo"/> for this property.
|
||||
/// </summary>
|
||||
public PropertyInfo PropertyInfo { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// A descriptor for method parameters of an action method.
|
||||
/// </summary>
|
||||
public class ControllerParameterDescriptor : ParameterDescriptor
|
||||
public class ControllerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ParameterInfo"/>.
|
||||
/// Gets or sets the <see cref="System.Reflection.ParameterInfo"/>.
|
||||
/// </summary>
|
||||
public ParameterInfo ParameterInfo { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ParameterDescriptor"/> for action parameters.
|
||||
/// </summary>
|
||||
public interface IParameterInfoParameterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="System.Reflection.ParameterInfo"/>.
|
||||
/// </summary>
|
||||
ParameterInfo ParameterInfo { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ParameterDescriptor"/> for bound properties.
|
||||
/// </summary>
|
||||
public interface IPropertyInfoParameterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="System.Reflection.PropertyInfo"/>.
|
||||
/// </summary>
|
||||
PropertyInfo PropertyInfo { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter)
|
||||
=> GetMetadataForParameter(parameter, parameter?.ParameterType);
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<MvcOptions> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="DefaultMetadataDetails"/> entry for a model <see cref="Type"/>.
|
||||
/// </summary>
|
||||
|
|
@ -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<ModelMetadataIdentity, ModelMetadataCacheEntry>
|
||||
|
|
|
|||
|
|
@ -135,9 +135,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// </returns>
|
||||
public static ModelAttributes GetAttributesForProperty(Type type, PropertyInfo property)
|
||||
{
|
||||
if (type == null)
|
||||
return GetAttributesForProperty(type, property, property.PropertyType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attributes for the given <paramref name="property"/> with the specified <paramref name="modelType"/>.
|
||||
/// </summary>
|
||||
/// <param name="containerType">The <see cref="Type"/> in which caller found <paramref name="property"/>.
|
||||
/// </param>
|
||||
/// <param name="property">A <see cref="PropertyInfo"/> for which attributes need to be resolved.
|
||||
/// </param>
|
||||
/// <param name="modelType">The model type</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ModelAttributes"/> instance with the attributes of the property and its <see cref="Type"/>.
|
||||
/// </returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attributes for the given <paramref name="parameterInfo"/> with the specified <paramref name="modelType"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameterInfo">
|
||||
/// The <see cref="ParameterInfo"/> for which attributes need to be resolved.
|
||||
/// </param>
|
||||
/// <param name="modelType">The model type.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ModelAttributes"/> instance with the attributes of the parameter and its <see cref="Type"/>.
|
||||
/// </returns>
|
||||
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<ModelMetadataTypeAttribute>()?.MetadataType;
|
||||
return type.GetCustomAttribute<ModelMetadataTypeAttribute>()?.MetadataType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="System.Reflection.ParameterInfo"/>.
|
||||
/// </summary>
|
||||
public ParameterInfo ParameterInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="System.Reflection.PropertyInfo"/> for this property.
|
||||
/// </summary>
|
||||
public PropertyInfo Property { get; set; }
|
||||
|
||||
PropertyInfo IPropertyInfoParameterDescriptor.PropertyInfo => Property;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DefaultModelMetadata>(metadata);
|
||||
|
||||
Assert.Collection(
|
||||
defaultModelMetadata.Attributes.Attributes,
|
||||
a => Assert.Equal("OnParameter", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnDerivedType", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnType", Assert.IsType<ModelAttribute>(a).Value));
|
||||
|
||||
Assert.Collection(
|
||||
metadata.Properties.OrderBy(p => p.Name),
|
||||
p =>
|
||||
{
|
||||
Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
|
||||
|
||||
var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
|
||||
Assert.Collection(
|
||||
defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
|
||||
a => Assert.Equal("OnDerivedProperty", Assert.IsType<ModelAttribute>(a).Value));
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
|
||||
|
||||
var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
|
||||
Assert.Collection(
|
||||
defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
|
||||
a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnPropertyType", Assert.IsType<ModelAttribute>(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<DefaultModelMetadata>(metadata);
|
||||
|
||||
Assert.Collection(
|
||||
defaultModelMetadata.Attributes.Attributes,
|
||||
a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnDerivedType", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnType", Assert.IsType<ModelAttribute>(a).Value));
|
||||
|
||||
Assert.Collection(
|
||||
metadata.Properties.OrderBy(p => p.Name),
|
||||
p =>
|
||||
{
|
||||
Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
|
||||
|
||||
var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
|
||||
Assert.Collection(
|
||||
defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
|
||||
a => Assert.Equal("OnDerivedProperty", Assert.IsType<ModelAttribute>(a).Value));
|
||||
},
|
||||
p =>
|
||||
{
|
||||
Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
|
||||
|
||||
var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
|
||||
Assert.Collection(
|
||||
defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
|
||||
a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
|
||||
a => Assert.Equal("OnPropertyType", Assert.IsType<ModelAttribute>(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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ClassValidator>(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<BindRequiredAttribute>(attribute),
|
||||
attribute => Assert.IsType<ModelBinderAttribute>(attribute),
|
||||
attribute => Assert.IsType<ClassValidator>(attribute));
|
||||
Assert.IsType<BindRequiredAttribute>(Assert.Single(attributes.ParameterAttributes));
|
||||
Assert.Null(attributes.PropertyAttributes);
|
||||
Assert.Collection(
|
||||
attributes.TypeAttributes,
|
||||
attribute => Assert.IsType<ModelBinderAttribute>(attribute),
|
||||
attribute => Assert.IsType<ClassValidator>(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<BindRequiredAttribute>(attribute),
|
||||
attribute => Assert.IsType<ModelBinderAttribute>(attribute),
|
||||
attribute => Assert.IsType<ClassValidator>(attribute));
|
||||
Assert.IsType<BindRequiredAttribute>(Assert.Single(attributes.PropertyAttributes));
|
||||
Assert.Null(attributes.ParameterAttributes);
|
||||
Assert.Collection(
|
||||
attributes.TypeAttributes,
|
||||
attribute => Assert.IsType<ModelBinderAttribute>(attribute),
|
||||
attribute => Assert.IsType<ClassValidator>(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
|
||||
|
|
|
|||
|
|
@ -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<IModelBinderFactory>(),
|
||||
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<IModelBinderFactory>(),
|
||||
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<IModelBinderFactory>(),
|
||||
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<IModelBinderFactory>(),
|
||||
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<Person> Kids { get; } = new List<Person>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<HttpResponseMessage> AssertStatusCodeAsync(this HttpResponseMessage response, HttpStatusCode expectedStatusCode)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
if (response.StatusCode == expectedStatusCode)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DerivedModel>(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<JArray>(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<DerivedModel>(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<JArray>(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<DerivedModel>(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<JArray>(p.Value);
|
||||
Assert.Equal("The DerivedProperty field is required.", value.First);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string>
|
||||
{
|
||||
{ "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<string, string>
|
||||
{
|
||||
{ "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<JArray>(p.Value);
|
||||
Assert.Equal("The field Age must be between 0 and 99.", value.First.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlerMethodArgumentsAndPropertiesAreModelBound()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MvcJsonOptions>(options => { options.SerializerSettings.Converters.Insert(0, new IModelConverter()); });
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@page
|
||||
@model PolymorphicBinding
|
||||
|
||||
<form action="">
|
||||
@Html.AntiForgeryToken()
|
||||
</form>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue