Use ModelMetadata from actual types for validation

Fixes https://github.com/aspnet/Mvc/issues/7952
This commit is contained in:
Pranav K 2018-06-26 12:34:04 -07:00
parent bd995d4cb1
commit 4f1b7ccca6
32 changed files with 1167 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
@page
@model PolymorphicBinding
<form action="">
@Html.AntiForgeryToken()
</form>

View File

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