From f6befb9ed333b0a76b24b8c438c181e08b1ad0ff Mon Sep 17 00:00:00 2001 From: Zbginiew Dobras Date: Mon, 25 Jun 2018 11:26:56 +0200 Subject: [PATCH 1/3] Added ObjectResult implementation for Unauthorized response --- .../ControllerBase.cs | 8 ++++++ .../UnauthorizedObjectResult.cs | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index 5626f1ccd2..c51f809da2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -1704,6 +1704,14 @@ namespace Microsoft.AspNetCore.Mvc public virtual UnauthorizedResult Unauthorized() => new UnauthorizedResult(); + /// + /// Creates an that produces a response. + /// + /// The created for the response. + [NonAction] + public virtual UnauthorizedObjectResult Unauthorized(object value) + => new UnauthorizedObjectResult(value); + /// /// Creates an that produces a response. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs new file mode 100644 index 0000000000..ebab2df60f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// An that when executed will produce a Unauthorized (401) response. + /// + [DefaultStatusCode(DefaultStatusCode)] + public class UnauthorizedObjectResult : ObjectResult + { + private const int DefaultStatusCode = StatusCodes.Status401Unauthorized; + + /// + /// Creates a new instance. + /// + public UnauthorizedObjectResult(object value) : base(value) + { + StatusCode = DefaultStatusCode; + } + } +} \ No newline at end of file From c2fcfabdf3f7bbcaaecbc77ecbfe358b3b7e0092 Mon Sep 17 00:00:00 2001 From: Alexej Timonin Date: Sat, 30 Jun 2018 01:02:38 +0200 Subject: [PATCH 2/3] Add optional property to PartialTagHelper (#7991) * Add optional to PartialTagHelper Addresses #7268 --- .../PartialTagHelper.cs | 39 +++++++++++----- .../PartialTagHelperTest.cs | 44 ++++++++++++++++++- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs index 1a463966cf..fef6b7c0e8 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { private const string ForAttributeName = "for"; private const string ModelAttributeName = "model"; + private const string OptionalAttributeName = "optional"; private object _model; private bool _hasModel; private bool _hasFor; @@ -74,6 +75,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } + /// + /// When optional, executing the tag helper will no-op if the view cannot be located. + /// Otherwise will throw stating the view could not be found. + /// + [HtmlAttributeName(OptionalAttributeName)] + public bool Optional { get; set; } + /// /// A to pass into the partial view. /// @@ -96,16 +104,21 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers throw new ArgumentNullException(nameof(context)); } - var model = ResolveModel(); - var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize); - using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8)) - { - await RenderPartialViewAsync(writer, model); + var viewEngineResult = FindView(); - // Reset the TagName. We don't want `partial` to render. - output.TagName = null; - output.Content.SetHtmlContent(viewBuffer); + if (viewEngineResult.Success) + { + var model = ResolveModel(); + var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize); + using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8)) + { + await RenderPartialViewAsync(writer, model, viewEngineResult.View); + output.Content.SetHtmlContent(viewBuffer); + } } + + // Reset the TagName. We don't want `partial` to render. + output.TagName = null; } // Internal for testing @@ -139,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return ViewContext.ViewData.Model; } - private async Task RenderPartialViewAsync(TextWriter writer, object model) + private ViewEngineResult FindView() { var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false); var getViewLocations = viewEngineResult.SearchedLocations; @@ -148,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false); } - if (!viewEngineResult.Success) + if (!viewEngineResult.Success && !Optional) { var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations); var locations = string.Empty; @@ -161,7 +174,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Resources.FormatViewEngine_PartialViewNotFound(Name, locations)); } - var view = viewEngineResult.View; + return viewEngineResult; + } + + private async Task RenderPartialViewAsync(TextWriter writer, object model, IView view) + { // Determine which ViewData we should use to construct a new ViewData var baseViewData = ViewData ?? ViewContext.ViewData; var newViewData = new ViewDataDictionary(baseViewData, model); diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs index 21ac445239..21edd86f7d 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs @@ -569,7 +569,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } [Fact] - public async Task ProcessAsync_Throws_IfGetViewAndFindReturnNotFoundResults() + public async Task ProcessAsync_Throws_If_NotOptional_And_GetViewAndFindReturnNotFoundResults() { // Arrange var bufferScope = new TestViewBufferScope(); @@ -596,6 +596,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Name = partialName, ViewContext = viewContext, ViewData = viewData, + Optional = false }; var tagHelperContext = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -605,6 +606,47 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers () => tagHelper.ProcessAsync(tagHelperContext, output)); Assert.Equal(expected, exception.Message); } + + [Fact] + public async Task ProcessAsync_IfOptional_And_ViewIsNotFound_WillNotRenderAnything() + { + // Arrange + var expected = string.Empty; + var bufferScope = new TestViewBufferScope(); + var partialName = "_ThisViewDoesNotExists"; + var model = new object(); + var viewContext = GetViewContext(); + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + v.Writer.Write(expected); + }) + .Returns(Task.CompletedTask); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, searchedLocations: Array.Empty())); + viewEngine.Setup(v => v.FindView(viewContext, partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, searchedLocations: Array.Empty())); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + Optional = true + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Empty(content); + } private static ViewContext GetViewContext() { From 335500ab0ed76e073514130068aab43a40ebba3e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 26 Jun 2018 12:34:04 -0700 Subject: [PATCH 3/3] Use ModelMetadata from actual types for validation Fixes https://github.com/aspnet/Mvc/issues/7952 --- .../Metadata/ModelMetadataIdentity.cs | 22 +- .../ModelBinding/ModelMetadataProvider.cs | 22 ++ .../ControllerBoundPropertyDescriptor.cs | 5 +- .../ControllerParameterDescriptor.cs | 5 +- .../IParameterInfoParameterDescriptor.cs | 19 ++ .../IPropertyInfoParameterDescriptor.cs | 19 ++ .../Metadata/DefaultModelMetadataProvider.cs | 113 ++++++-- .../ModelBinding/Metadata/ModelAttributes.cs | 61 +++- .../ModelBinding/ParameterBinder.cs | 38 +++ .../HandlerParameterDescriptor.cs | 6 +- .../PageBoundPropertyDescriptor.cs | 8 +- .../DefaultModelMetadataProviderTest.cs | 181 ++++++++++++ .../Metadata/ModelAttributesTest.cs | 58 +++- .../ModelBinding/ParameterBinderTest.cs | 263 ++++++++++++++++++ .../BasicTests.cs | 3 +- .../Infrastructure/HttpClientExtensions.cs | 3 +- .../InputFormatterTests.cs | 98 ++++++- .../RazorPagesTest.cs | 60 ++++ test/WebSites/BasicWebSite/Startup.cs | 3 +- .../PolymorhpicPropertyBindingController.cs | 25 ++ .../PolymorphicBindingController.cs | 32 +++ .../FormatterWebSite/IModelConverter.cs | 30 ++ .../FormatterWebSite/Models/BaseModel.cs | 10 + .../FormatterWebSite/Models/DerivedModel.cs | 14 + .../FormatterWebSite/Models/IModel.cs | 9 + .../FormatterWebSite/PolymorphicBinder.cs | 24 ++ test/WebSites/FormatterWebSite/Startup.cs | 3 + .../RazorPagesWebSite/Models/IUserModel.cs | 9 + .../RazorPagesWebSite/Models/UserModel.cs | 7 +- .../PropertyBinding/PolymorphicBinding.cs | 24 ++ .../PropertyBinding/PolymorphicBinding.cshtml | 6 + .../PolymorphicModelBinder.cs | 32 +++ 32 files changed, 1167 insertions(+), 45 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs create mode 100644 test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs create mode 100644 test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs create mode 100644 test/WebSites/FormatterWebSite/IModelConverter.cs create mode 100644 test/WebSites/FormatterWebSite/Models/BaseModel.cs create mode 100644 test/WebSites/FormatterWebSite/Models/DerivedModel.cs create mode 100644 test/WebSites/FormatterWebSite/Models/IModel.cs create mode 100644 test/WebSites/FormatterWebSite/PolymorphicBinder.cs create mode 100644 test/WebSites/RazorPagesWebSite/Models/IUserModel.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs index 782b84b2cd..af97d9a043 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs @@ -66,17 +66,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata }; } + /// + /// Creates a for the provided parameter. + /// + /// The . + /// A . public static ModelMetadataIdentity ForParameter(ParameterInfo parameter) + => ForParameter(parameter, parameter?.ParameterType); + + /// + /// Creates a for the provided parameter with the specified + /// model type. + /// + /// The . + /// The model type. + /// A . + public static ModelMetadataIdentity ForParameter(ParameterInfo parameter, Type modelType) { if (parameter == null) { throw new ArgumentNullException(nameof(parameter)); } + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + return new ModelMetadataIdentity() { Name = parameter.Name, - ModelType = parameter.ParameterType, + ModelType = modelType, ParameterInfo = parameter, }; } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs index 1b4f01cd6d..147ccc45f5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs @@ -32,5 +32,27 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// The . /// A instance describing the . public abstract ModelMetadata GetMetadataForParameter(ParameterInfo parameter); + + /// + /// Supplies metadata describing a parameter. + /// + /// The + /// The actual model type. + /// A instance describing the . + public virtual ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType) + { + throw new NotSupportedException(); + } + + /// + /// Supplies metadata describing a property. + /// + /// The . + /// The actual model type. + /// A instance describing the . + public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType) + { + throw new NotSupportedException(); + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs index 7c41dda166..709dffcf5c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs @@ -3,16 +3,17 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc.Controllers { /// /// A descriptor for model bound properties of a controller. /// - public class ControllerBoundPropertyDescriptor : ParameterDescriptor + public class ControllerBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor { /// - /// Gets or sets the for this property. + /// Gets or sets the for this property. /// public PropertyInfo PropertyInfo { get; set; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs index e48ef9dd00..593a40d0fc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs @@ -3,16 +3,17 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc.Controllers { /// /// A descriptor for method parameters of an action method. /// - public class ControllerParameterDescriptor : ParameterDescriptor + public class ControllerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor { /// - /// Gets or sets the . + /// Gets or sets the . /// public ParameterInfo ParameterInfo { get; set; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs new file mode 100644 index 0000000000..93e6a09b28 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Abstractions; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// A for action parameters. + /// + public interface IParameterInfoParameterDescriptor + { + /// + /// Gets the . + /// + ParameterInfo ParameterInfo { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs new file mode 100644 index 0000000000..5a9a8682d1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Abstractions; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// A for bound properties. + /// + public interface IPropertyInfoParameterDescriptor + { + /// + /// Gets the . + /// + PropertyInfo PropertyInfo { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs index 5b79143002..b9e374701e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; @@ -97,14 +98,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata return cacheEntry.Details.Properties; } + /// public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter) + => GetMetadataForParameter(parameter, parameter?.ParameterType); + + /// + public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType) { if (parameter == null) { throw new ArgumentNullException(nameof(parameter)); } - var cacheEntry = GetCacheEntry(parameter); + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + + var cacheEntry = GetCacheEntry(parameter, modelType); return cacheEntry.Metadata; } @@ -122,6 +133,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata return cacheEntry.Metadata; } + /// + public override ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + + var cacheEntry = GetCacheEntry(propertyInfo, modelType); + + return cacheEntry.Metadata; + } + private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions optionsAccessor) { if (optionsAccessor == null) @@ -151,10 +180,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata return cacheEntry; } - private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter) + private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType) { return _typeCache.GetOrAdd( - ModelMetadataIdentity.ForParameter(parameter), + ModelMetadataIdentity.ForParameter(parameter, modelType), + _cacheEntryFactory); + } + + private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType) + { + return _typeCache.GetOrAdd( + ModelMetadataIdentity.ForProperty(modelType, property.Name, property.DeclaringType), _cacheEntryFactory); } @@ -165,6 +201,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata { details = CreateParameterDetails(key); } + else if (key.MetadataKind == ModelMetadataKind.Property) + { + details = CreateSinglePropertyDetails(key); + } else { details = CreateTypeDetails(key); @@ -174,6 +214,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata return new ModelMetadataCacheEntry(metadata, details); } + private DefaultMetadataDetails CreateSinglePropertyDetails(ModelMetadataIdentity propertyKey) + { + var propertyHelpers = PropertyHelper.GetVisibleProperties(propertyKey.ContainerType); + for (var i = 0; i < propertyHelpers.Length; i++) + { + var propertyHelper = propertyHelpers[i]; + if (propertyHelper.Name == propertyKey.Name) + { + return CreateSinglePropertyDetails(propertyKey, propertyHelper); + } + } + + Debug.Fail($"Unable to find property '{propertyKey.Name}' on type '{propertyKey.ContainerType}."); + return null; + } + private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType() { var key = ModelMetadataIdentity.ForType(typeof(object)); @@ -217,35 +273,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata for (var i = 0; i < propertyHelpers.Length; i++) { var propertyHelper = propertyHelpers[i]; + var propertyKey = ModelMetadataIdentity.ForProperty( propertyHelper.Property.PropertyType, propertyHelper.Name, key.ModelType); - var attributes = ModelAttributes.GetAttributesForProperty( - key.ModelType, - propertyHelper.Property); - - var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes); - if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true) - { - var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property); - propertyEntry.PropertyGetter = getter; - } - - if (propertyHelper.Property.CanWrite && - propertyHelper.Property.SetMethod?.IsPublic == true && - !key.ModelType.GetTypeInfo().IsValueType) - { - propertyEntry.PropertySetter = propertyHelper.ValueSetter; - } - + var propertyEntry = CreateSinglePropertyDetails(propertyKey, propertyHelper); propertyEntries.Add(propertyEntry); } return propertyEntries.ToArray(); } + private DefaultMetadataDetails CreateSinglePropertyDetails( + ModelMetadataIdentity propertyKey, + PropertyHelper propertyHelper) + { + Debug.Assert(propertyKey.MetadataKind == ModelMetadataKind.Property); + var containerType = propertyKey.ContainerType; + + var attributes = ModelAttributes.GetAttributesForProperty( + containerType, + propertyHelper.Property, + propertyKey.ModelType); + + var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes); + if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true) + { + var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property); + propertyEntry.PropertyGetter = getter; + } + + if (propertyHelper.Property.CanWrite && + propertyHelper.Property.SetMethod?.IsPublic == true && + !containerType.IsValueType) + { + propertyEntry.PropertySetter = propertyHelper.ValueSetter; + } + + return propertyEntry; + } + /// /// Creates the entry for a model . /// @@ -269,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata { return new DefaultMetadataDetails( key, - ModelAttributes.GetAttributesForParameter(key.ParameterInfo)); + ModelAttributes.GetAttributesForParameter(key.ParameterInfo, key.ModelType)); } private class TypeCache : ConcurrentDictionary diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs index e5b59933ae..0f9a5bf103 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs @@ -135,9 +135,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public static ModelAttributes GetAttributesForProperty(Type type, PropertyInfo property) { - if (type == null) + return GetAttributesForProperty(type, property, property.PropertyType); + } + + /// + /// Gets the attributes for the given with the specified . + /// + /// The in which caller found . + /// + /// A for which attributes need to be resolved. + /// + /// The model type + /// + /// A instance with the attributes of the property and its . + /// + public static ModelAttributes GetAttributesForProperty(Type containerType, PropertyInfo property, Type modelType) + { + if (containerType == null) { - throw new ArgumentNullException(nameof(type)); + throw new ArgumentNullException(nameof(containerType)); } if (property == null) @@ -146,9 +162,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } var propertyAttributes = property.GetCustomAttributes(); - var typeAttributes = property.PropertyType.GetTypeInfo().GetCustomAttributes(); + var typeAttributes = modelType.GetCustomAttributes(); - var metadataType = GetMetadataType(type); + var metadataType = GetMetadataType(containerType); if (metadataType != null) { var metadataProperty = metadataType.GetRuntimeProperty(property.Name); @@ -174,12 +190,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding throw new ArgumentNullException(nameof(type)); } - var attributes = type.GetTypeInfo().GetCustomAttributes(); + var attributes = type.GetCustomAttributes(); var metadataType = GetMetadataType(type); if (metadataType != null) { - attributes = attributes.Concat(metadataType.GetTypeInfo().GetCustomAttributes()); + attributes = attributes.Concat(metadataType.GetCustomAttributes()); } return new ModelAttributes(attributes, propertyAttributes: null, parameterAttributes: null); @@ -205,9 +221,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes); } + /// + /// Gets the attributes for the given with the specified . + /// + /// + /// The for which attributes need to be resolved. + /// + /// The model type. + /// + /// A instance with the attributes of the parameter and its . + /// + public static ModelAttributes GetAttributesForParameter(ParameterInfo parameterInfo, Type modelType) + { + if (parameterInfo == null) + { + throw new ArgumentNullException(nameof(parameterInfo)); + } + + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + + // Prior versions called IModelMetadataProvider.GetMetadataForType(...) and therefore + // GetAttributesForType(...) for parameters. Maintain that set of attributes (including those from an + // ModelMetadataTypeAttribute reference) for back-compatibility. + var typeAttributes = GetAttributesForType(modelType).TypeAttributes; + var parameterAttributes = parameterInfo.GetCustomAttributes(); + + return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes); + } + private static Type GetMetadataType(Type type) { - return type.GetTypeInfo().GetCustomAttribute()?.MetadataType; + return type.GetCustomAttribute()?.MetadataType; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs index 973058609b..51ed9c3034 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Logging; @@ -283,6 +284,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]) @@ -330,5 +333,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding metadata); } } + + private void RecalculateModelMetadata( + ParameterDescriptor parameter, + ModelBindingResult modelBindingResult, + ref ModelMetadata metadata) + { + // Attempt to recalculate ModelMetadata for top level parameters and properties using the actual + // model type. This ensures validation uses a combination of top-level validation metadata + // as well as metadata on the actual, rather than declared, model type. + + if (!modelBindingResult.IsModelSet || + modelBindingResult.Model == null || + !(_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider)) + { + return; + } + + var modelType = modelBindingResult.Model.GetType(); + if (parameter is IParameterInfoParameterDescriptor parameterInfoParameter) + { + var parameterInfo = parameterInfoParameter.ParameterInfo; + if (modelType != parameterInfo.ParameterType) + { + metadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo, modelType); + } + } + else if (parameter is IPropertyInfoParameterDescriptor propertyInfoParameter) + { + var propertyInfo = propertyInfoParameter.PropertyInfo; + if (modelType != propertyInfo.PropertyType) + { + metadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, modelType); + } + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs index dcf212e077..099b86192c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs @@ -3,11 +3,15 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { - public class HandlerParameterDescriptor : ParameterDescriptor + public class HandlerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor { + /// + /// Gets or sets the . + /// public ParameterInfo ParameterInfo { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs index 56cbdc40f7..cb02222300 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs @@ -3,11 +3,17 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { - public class PageBoundPropertyDescriptor : ParameterDescriptor + public class PageBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor { + /// + /// Gets or sets the for this property. + /// public PropertyInfo Property { get; set; } + + PropertyInfo IPropertyInfoParameterDescriptor.PropertyInfo => Property; } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs index 4286f263c9..8994835817 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Options; using Xunit; @@ -263,6 +264,173 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata Assert.Same(metadata1, metadata2); } + [Fact] + public void GetMetadataForParameter_WithModelType_ReturnsCombinedModelMetadata() + { + // Arrange + var parameter = GetType() + .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance) + .GetParameters()[0]; + var provider = CreateProvider(); + + // Act + var metadata = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType)); + + // Assert + Assert.Equal(ModelMetadataKind.Parameter, metadata.MetadataKind); + Assert.Equal(typeof(DerivedModelType), metadata.ModelType); + + var defaultModelMetadata = Assert.IsType(metadata); + + Assert.Collection( + defaultModelMetadata.Attributes.Attributes, + a => Assert.Equal("OnParameter", Assert.IsType(a).Value), + a => Assert.Equal("OnDerivedType", Assert.IsType(a).Value), + a => Assert.Equal("OnType", Assert.IsType(a).Value)); + + Assert.Collection( + metadata.Properties.OrderBy(p => p.Name), + p => + { + Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name); + + var defaultPropertyMetadata = Assert.IsType(p); + Assert.Collection( + defaultPropertyMetadata.Attributes.Attributes.OfType(), + a => Assert.Equal("OnDerivedProperty", Assert.IsType(a).Value)); + }, + p => + { + Assert.Equal(nameof(DerivedModelType.Property1), p.Name); + + var defaultPropertyMetadata = Assert.IsType(p); + Assert.Collection( + defaultPropertyMetadata.Attributes.Attributes.OfType(), + a => Assert.Equal("OnProperty", Assert.IsType(a).Value), + a => Assert.Equal("OnPropertyType", Assert.IsType(a).Value)); + }, + p => + { + Assert.Equal(nameof(DerivedModelType.Property2), p.Name); + }); + } + + [Fact] + public void GetMetadataForParameter_WithModelType_CachesResults() + { + // Arrange + var parameter = GetType() + .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance) + .GetParameters()[0]; + var provider = CreateProvider(); + + // Act + var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType)); + var metadata2 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType)); + + // Assert + Assert.Same(metadata1, metadata2); + } + + [Fact] + public void GetMetadataForParameter_WithModelType_VariesByModelType() + { + // Arrange + var parameter = GetType() + .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance) + .GetParameters()[0]; + var provider = CreateProvider(); + + // Act + var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType)); + var metadata2 = provider.GetMetadataForParameter(parameter, typeof(object)); + + // Assert + Assert.NotSame(metadata1, metadata2); + } + + [Fact] + public void GetMetadataForProperty_WithModelType_ReturnsCombinedModelMetadata() + { + // Arrange + var property = typeof(TestContainer) + .GetProperty(nameof(TestContainer.ModelProperty)); + var provider = CreateProvider(); + + // Act + var metadata = provider.GetMetadataForProperty(property, typeof(DerivedModelType)); + + // Assert + Assert.Equal(ModelMetadataKind.Property, metadata.MetadataKind); + Assert.Equal(typeof(DerivedModelType), metadata.ModelType); + + var defaultModelMetadata = Assert.IsType(metadata); + + Assert.Collection( + defaultModelMetadata.Attributes.Attributes, + a => Assert.Equal("OnProperty", Assert.IsType(a).Value), + a => Assert.Equal("OnDerivedType", Assert.IsType(a).Value), + a => Assert.Equal("OnType", Assert.IsType(a).Value)); + + Assert.Collection( + metadata.Properties.OrderBy(p => p.Name), + p => + { + Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name); + + var defaultPropertyMetadata = Assert.IsType(p); + Assert.Collection( + defaultPropertyMetadata.Attributes.Attributes.OfType(), + a => Assert.Equal("OnDerivedProperty", Assert.IsType(a).Value)); + }, + p => + { + Assert.Equal(nameof(DerivedModelType.Property1), p.Name); + + var defaultPropertyMetadata = Assert.IsType(p); + Assert.Collection( + defaultPropertyMetadata.Attributes.Attributes.OfType(), + a => Assert.Equal("OnProperty", Assert.IsType(a).Value), + a => Assert.Equal("OnPropertyType", Assert.IsType(a).Value)); + }, + p => + { + Assert.Equal(nameof(DerivedModelType.Property2), p.Name); + }); + } + + [Fact] + public void GetMetadataForProperty_WithModelType_CachesResults() + { + // Arrange + var property = typeof(TestContainer) + .GetProperty(nameof(TestContainer.ModelProperty)); + var provider = CreateProvider(); + + // Act + var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType)); + var metadata2 = provider.GetMetadataForProperty(property, typeof(DerivedModelType)); + + // Assert + Assert.Same(metadata1, metadata2); + } + + [Fact] + public void GetMetadataForProperty_WithModelType_VariesByModelType() + { + // Arrange + var property = typeof(TestContainer) + .GetProperty(nameof(TestContainer.ModelProperty)); + var provider = CreateProvider(); + + // Act + var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType)); + var metadata2 = provider.GetMetadataForProperty(property, typeof(object)); + + // Assert + Assert.NotSame(metadata1, metadata2); + } + private static DefaultModelMetadataProvider CreateProvider() { return new DefaultModelMetadataProvider( @@ -321,5 +489,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata { public new string Property { get; set; } } + + [Model("OnDerivedType")] + private class DerivedModelType : ModelType + { + [Model("OnDerivedProperty")] + public string DerivedProperty { get; set; } + } + + private class TestContainer + { + [Model("OnProperty")] + public ModelType ModelProperty { get; set; } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs index 3b0b3b0d92..3174e0110d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/ModelAttributesTest.cs @@ -5,7 +5,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -242,6 +241,55 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding Assert.IsType(Assert.Single(attributes.TypeAttributes)); } + [Fact] + public void GetAttributesForParameter_WithModelType_IncludesTypeAttributes() + { + // Arrange + var parameters = typeof(MethodWithParamAttributesType) + .GetMethod(nameof(MethodWithParamAttributesType.Method)) + .GetParameters(); + + // Act + var attributes = ModelAttributes.GetAttributesForParameter(parameters[2], typeof(DerivedModelWithAttributes)); + + // Assert + Assert.Collection( + attributes.Attributes, + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute)); + Assert.IsType(Assert.Single(attributes.ParameterAttributes)); + Assert.Null(attributes.PropertyAttributes); + Assert.Collection( + attributes.TypeAttributes, + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute)); + } + + [Fact] + public void GetAttributesForProperty_WithModelType_IncludesTypeAttributes() + { + // Arrange + var property = typeof(MergedAttributes) + .GetProperty(nameof(MergedAttributes.BaseModel)); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(typeof(MergedAttributes), property, typeof(DerivedModelWithAttributes)); + + // Assert + Assert.Collection( + attributes.Attributes, + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute)); + Assert.IsType(Assert.Single(attributes.PropertyAttributes)); + Assert.Null(attributes.ParameterAttributes); + Assert.Collection( + attributes.TypeAttributes, + attribute => Assert.IsType(attribute), + attribute => Assert.IsType(attribute)); + } + [ClassValidator] private class BaseModel { @@ -272,6 +320,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } + [ModelBinder(Name = "Custom")] + private class DerivedModelWithAttributes : BaseModel + { + } + [ModelMetadataType(typeof(BaseModel))] private class BaseViewModel { @@ -313,6 +366,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { [Required] public PropertyType Property { get; set; } + + [BindRequired] + public BaseModel BaseModel { get; set; } } private class MergedAttributesMetadata diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs index 6a49060f19..7a605c4640 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ParameterBinderTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -491,6 +492,220 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage); } + [Fact] + public async Task BindModelAsync_ForParameter_UsesValidationFromActualModel_WhenDerivedModelIsSet() + { + // Arrange + var method = GetType().GetMethod(nameof(TestMethodWithoutAttributes), BindingFlags.NonPublic | BindingFlags.Instance); + var parameter = method.GetParameters()[0]; + var parameterDescriptor = new ControllerParameterDescriptor + { + ParameterInfo = parameter, + Name = parameter.Name, + }; + + var actionContext = GetControllerContext(); + var modelMetadataProvider = new TestModelMetadataProvider(); + + var model = new DerivedPerson(); + var modelBindingResult = ModelBindingResult.Success(model); + + var parameterBinder = new ParameterBinder( + modelMetadataProvider, + Mock.Of(), + new DefaultObjectValidator( + modelMetadataProvider, + new[] { TestModelValidatorProvider.CreateDefaultProvider() }), + _optionsAccessor, + NullLoggerFactory.Instance); + + var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter); + var modelBinder = CreateMockModelBinder(modelBindingResult); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + modelBinder, + CreateMockValueProvider(), + parameterDescriptor, + modelMetadata, + value: null); + + // Assert + Assert.True(result.IsModelSet); + Assert.Same(model, result.Model); + + Assert.False(actionContext.ModelState.IsValid); + Assert.Collection( + actionContext.ModelState, + kvp => + { + Assert.Equal($"{parameter.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key); + var error = Assert.Single(kvp.Value.Errors); + Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage); + }); + } + + [Fact] + public async Task BindModelAsync_ForParameter_UsesValidationFromParameter_WhenDerivedModelIsSet() + { + // Arrange + var method = GetType().GetMethod(nameof(TestMethodWithAttributes), BindingFlags.NonPublic | BindingFlags.Instance); + var parameter = method.GetParameters()[0]; + var parameterDescriptor = new ControllerParameterDescriptor + { + ParameterInfo = parameter, + Name = parameter.Name, + }; + + var actionContext = GetControllerContext(); + var modelMetadataProvider = new TestModelMetadataProvider(); + + var model = new DerivedPerson { DerivedProperty = "SomeValue" }; + var modelBindingResult = ModelBindingResult.Success(model); + + var parameterBinder = new ParameterBinder( + modelMetadataProvider, + Mock.Of(), + new DefaultObjectValidator( + modelMetadataProvider, + new[] { TestModelValidatorProvider.CreateDefaultProvider() }), + _optionsAccessor, + NullLoggerFactory.Instance); + + var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter); + var modelBinder = CreateMockModelBinder(modelBindingResult); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + modelBinder, + CreateMockValueProvider(), + parameterDescriptor, + modelMetadata, + value: null); + + // Assert + Assert.True(result.IsModelSet); + Assert.Same(model, result.Model); + + Assert.False(actionContext.ModelState.IsValid); + Assert.Collection( + actionContext.ModelState, + kvp => + { + Assert.Equal(parameter.Name, kvp.Key); + var error = Assert.Single(kvp.Value.Errors); + Assert.Equal("Always Invalid", error.ErrorMessage); + }); + } + + [Fact] + public async Task BindModelAsync_ForProperty_UsesValidationFromActualModel_WhenDerivedModelIsSet() + { + // Arrange + var property = typeof(TestController).GetProperty(nameof(TestController.Model)); + var parameterDescriptor = new ControllerBoundPropertyDescriptor + { + PropertyInfo = property, + Name = property.Name, + }; + + var actionContext = GetControllerContext(); + var modelMetadataProvider = new TestModelMetadataProvider(); + + var model = new DerivedModel(); + var modelBindingResult = ModelBindingResult.Success(model); + + var parameterBinder = new ParameterBinder( + modelMetadataProvider, + Mock.Of(), + new DefaultObjectValidator( + modelMetadataProvider, + new[] { TestModelValidatorProvider.CreateDefaultProvider() }), + _optionsAccessor, + NullLoggerFactory.Instance); + + var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name); + var modelBinder = CreateMockModelBinder(modelBindingResult); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + modelBinder, + CreateMockValueProvider(), + parameterDescriptor, + modelMetadata, + value: null); + + // Assert + Assert.True(result.IsModelSet); + Assert.Same(model, result.Model); + + Assert.False(actionContext.ModelState.IsValid); + Assert.Collection( + actionContext.ModelState, + kvp => + { + Assert.Equal($"{property.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key); + var error = Assert.Single(kvp.Value.Errors); + Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage); + }); + } + + [Fact] + public async Task BindModelAsync_ForProperty_UsesValidationOnProperty_WhenDerivedModelIsSet() + { + // Arrange + var property = typeof(TestControllerWithValidatedProperties).GetProperty(nameof(TestControllerWithValidatedProperties.Model)); + var parameterDescriptor = new ControllerBoundPropertyDescriptor + { + PropertyInfo = property, + Name = property.Name, + }; + + var actionContext = GetControllerContext(); + var modelMetadataProvider = new TestModelMetadataProvider(); + + var model = new DerivedModel { DerivedProperty = "some value" }; + var modelBindingResult = ModelBindingResult.Success(model); + + var parameterBinder = new ParameterBinder( + modelMetadataProvider, + Mock.Of(), + new DefaultObjectValidator( + modelMetadataProvider, + new[] { TestModelValidatorProvider.CreateDefaultProvider() }), + _optionsAccessor, + NullLoggerFactory.Instance); + + var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name); + var modelBinder = CreateMockModelBinder(modelBindingResult); + + // Act + var result = await parameterBinder.BindModelAsync( + actionContext, + modelBinder, + CreateMockValueProvider(), + parameterDescriptor, + modelMetadata, + value: null); + + // Assert + Assert.True(result.IsModelSet); + Assert.Same(model, result.Model); + + Assert.False(actionContext.ModelState.IsValid); + Assert.Collection( + actionContext.ModelState, + kvp => + { + Assert.Equal($"{property.Name}", kvp.Key); + var error = Assert.Single(kvp.Value.Errors); + Assert.Equal("Always Invalid", error.ErrorMessage); + }); + } + private static ControllerContext GetControllerContext() { var services = new ServiceCollection(); @@ -641,6 +856,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public IList Kids { get; } = new List(); } + private class DerivedPerson : Person + { + [Required] + public string DerivedProperty { get; set; } + } + + [Required] + private Person PersonProperty { get; set; } + public abstract class FakeModelMetadata : ModelMetadata { public FakeModelMetadata() @@ -648,5 +872,44 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { } } + + private void TestMethodWithoutAttributes(Person person) { } + + private void TestMethodWithAttributes([Required][AlwaysInvalid] Person person) { } + + private class TestController + { + public BaseModel Model { get; set; } + } + + private class TestControllerWithValidatedProperties + { + [AlwaysInvalid] + [Required] + public BaseModel Model { get; set; } + } + + private class BaseModel + { + } + + private class DerivedModel + { + [Required] + public string DerivedProperty { get; set; } + } + + private class AlwaysInvalidAttribute : ValidationAttribute + { + public AlwaysInvalidAttribute() + { + ErrorMessage = "Always Invalid"; + } + + public override bool IsValid(object value) + { + return false; + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index c93092b339..6761579f04 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -3,15 +3,14 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; -using System.Text; using System.Threading.Tasks; using BasicWebSite.Models; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs index 590e1d02a9..fe8d4f300b 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/HttpClientExtensions.cs @@ -7,7 +7,6 @@ using System.Net.Http; using System.Threading.Tasks; using AngleSharp.Dom.Html; using AngleSharp.Parser.Html; -using Xunit; using Xunit.Sdk; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -32,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public static async Task AssertStatusCodeAsync(this HttpResponseMessage response, HttpStatusCode expectedStatusCode) { - if (response.StatusCode == HttpStatusCode.OK) + if (response.StatusCode == expectedStatusCode) { return response; } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs index 2907b248c8..2425c21f3a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs @@ -3,11 +3,12 @@ using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; +using FormatterWebSite.Models; using Microsoft.AspNetCore.Testing.xunit; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -217,5 +218,100 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); } + + [Fact] + public async Task BindingWorksForPolymorphicTypes() + { + // Act + var response = await Client.GetAsync("PolymorphicBinding/ModelBound?DerivedProperty=Test"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + Assert.Equal("Test", result.DerivedProperty); + } + + [Fact] + public async Task ValidationUsesModelMetadataFromActualModelType_ForModelBoundParameters() + { + // Act + var response = await Client.GetAsync("PolymorphicBinding/ModelBound"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Collection( + result.Properties(), + p => + { + Assert.Equal("DerivedProperty", p.Name); + var value = Assert.IsType(p.Value); + Assert.Equal("The DerivedProperty field is required.", value.First); + }); + } + + [Fact] + public async Task InputFormatterWorksForPolymorphicTypes() + { + // Act + var input = "Test"; + var response = await Client.PostAsJsonAsync("PolymorphicBinding/InputFormatted", input); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + Assert.Equal(input, result.DerivedProperty); + } + + [Fact] + public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormattedParameters() + { + // Act + var response = await Client.PostAsJsonAsync("PolymorphicBinding/InputFormatted", string.Empty); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Collection( + result.Properties(), + p => + { + Assert.Equal("DerivedProperty", p.Name); + var value = Assert.IsType(p.Value); + Assert.Equal("The DerivedProperty field is required.", value.First); + }); + } + + [Fact] + public async Task InputFormatterWorksForPolymorphicProperties() + { + // Act + var input = "Test"; + var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", input); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + Assert.Equal(input, result.DerivedProperty); + } + + [Fact] + public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormattedProperties() + { + // Act + var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", string.Empty); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Collection( + result.Properties(), + p => + { + Assert.Equal("DerivedProperty", p.Name); + var value = Assert.IsType(p.Value); + Assert.Equal("The DerivedProperty field is required.", value.First); + }); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 524f8bd977..d97e128727 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Testing; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -738,6 +739,65 @@ Hello from /Pages/WithViewStart/Index.cshtml!"; } } + [Fact] + public async Task PolymorphicPropertiesOnPageModelsAreBound() + { + // Arrange + var name = "TestName"; + var age = 23; + var expected = $"Name = {name}, Age = {age}"; + var request = new HttpRequestMessage(HttpMethod.Post, "Pages/PropertyBinding/PolymorphicBinding") + { + Content = new FormUrlEncodedContent(new Dictionary + { + { "Name", name }, + { "Age", age.ToString() }, + }), + }; + await AddAntiforgeryHeaders(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, content); + } + + [Fact] + public async Task PolymorphicPropertiesOnPageModelsAreValidated() + { + // Arrange + var name = "TestName"; + var age = 123; + var expected = $"Name = {name}, Age = {age}"; + var request = new HttpRequestMessage(HttpMethod.Post, "Pages/PropertyBinding/PolymorphicBinding") + { + Content = new FormUrlEncodedContent(new Dictionary + { + { "Name", name }, + { "Age", age.ToString() }, + }), + }; + await AddAntiforgeryHeaders(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Collection( + result.Properties(), + p => + { + Assert.Equal("Age", p.Name); + var value = Assert.IsType(p.Value); + Assert.Equal("The field Age must be between 0 and 99.", value.First.ToString()); + }); + } + [Fact] public async Task HandlerMethodArgumentsAndPropertiesAreModelBound() { diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index 9d4db8b9da..9e20cb447c 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; -using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -36,7 +35,7 @@ namespace BasicWebSite var previous = options.InvalidModelStateResponseFactory; options.InvalidModelStateResponseFactory = context => { - var result = (BadRequestObjectResult) previous(context); + var result = (BadRequestObjectResult)previous(context); if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute)) { result.ContentTypes.Clear(); diff --git a/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs b/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs new file mode 100644 index 0000000000..27a2643cfc --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using FormatterWebSite.Models; +using Microsoft.AspNetCore.Mvc; + +namespace FormatterWebSite.Controllers +{ + public class PolymorhpicPropertyBindingController : ControllerBase + { + [FromBody] + public IModel Person { get; set; } + + [HttpPost] + public IActionResult Action() + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return Ok(Person); + } + } +} diff --git a/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs b/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs new file mode 100644 index 0000000000..9358ec50d6 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/PolymorphicBindingController.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using FormatterWebSite.Models; +using Microsoft.AspNetCore.Mvc; + +namespace FormatterWebSite.Controllers +{ + public class PolymorphicBindingController : ControllerBase + { + public IActionResult ModelBound([ModelBinder(typeof(PolymorphicBinder))] BaseModel person) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return Ok(person); + } + + [HttpPost] + public IActionResult InputFormatted([FromBody] IModel person) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return Ok(person); + } + } +} diff --git a/test/WebSites/FormatterWebSite/IModelConverter.cs b/test/WebSites/FormatterWebSite/IModelConverter.cs new file mode 100644 index 0000000000..1ce4131f17 --- /dev/null +++ b/test/WebSites/FormatterWebSite/IModelConverter.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using FormatterWebSite.Models; +using Newtonsoft.Json; + +namespace FormatterWebSite +{ + public class IModelConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IModel); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return new DerivedModel + { + DerivedProperty = reader.Value.ToString(), + }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Models/BaseModel.cs b/test/WebSites/FormatterWebSite/Models/BaseModel.cs new file mode 100644 index 0000000000..b7b6567b1e --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/BaseModel.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace FormatterWebSite.Models +{ + public class BaseModel + { + public string BaseProperty { get; set; } + } +} diff --git a/test/WebSites/FormatterWebSite/Models/DerivedModel.cs b/test/WebSites/FormatterWebSite/Models/DerivedModel.cs new file mode 100644 index 0000000000..2b10ac357a --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/DerivedModel.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace FormatterWebSite.Models +{ + public class DerivedModel : BaseModel, IModel + { + [Required] + [StringLength(10)] + public string DerivedProperty { get; set; } + } +} diff --git a/test/WebSites/FormatterWebSite/Models/IModel.cs b/test/WebSites/FormatterWebSite/Models/IModel.cs new file mode 100644 index 0000000000..ce6e4b6d9b --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/IModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace FormatterWebSite.Models +{ + public interface IModel + { + } +} diff --git a/test/WebSites/FormatterWebSite/PolymorphicBinder.cs b/test/WebSites/FormatterWebSite/PolymorphicBinder.cs new file mode 100644 index 0000000000..e7eefb71c2 --- /dev/null +++ b/test/WebSites/FormatterWebSite/PolymorphicBinder.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using FormatterWebSite.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace FormatterWebSite.Controllers +{ + public class PolymorphicBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var model = new DerivedModel + { + DerivedProperty = bindingContext.ValueProvider.GetValue(nameof(DerivedModel.DerivedProperty)).FirstValue, + }; + + bindingContext.Result = ModelBindingResult.Success(model); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Startup.cs b/test/WebSites/FormatterWebSite/Startup.cs index dd3beb9121..ee2744a028 100644 --- a/test/WebSites/FormatterWebSite/Startup.cs +++ b/test/WebSites/FormatterWebSite/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,8 @@ namespace FormatterWebSite options.InputFormatters.Add(new StringInputFormatter()); }) .AddXmlDataContractSerializerFormatters(); + + services.Configure(options => { options.SerializerSettings.Converters.Insert(0, new IModelConverter()); }); } diff --git a/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs b/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs new file mode 100644 index 0000000000..19cc1d40a9 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Models/IUserModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace RazorPagesWebSite +{ + public interface IUserModel + { + } +} diff --git a/test/WebSites/RazorPagesWebSite/Models/UserModel.cs b/test/WebSites/RazorPagesWebSite/Models/UserModel.cs index 62c2bf8cae..80c2d8fdc8 100644 --- a/test/WebSites/RazorPagesWebSite/Models/UserModel.cs +++ b/test/WebSites/RazorPagesWebSite/Models/UserModel.cs @@ -5,12 +5,17 @@ using System.ComponentModel.DataAnnotations; namespace RazorPagesWebSite { - public class UserModel + public class UserModel : IUserModel { [Required] public string Name { get; set; } [Range(0, 99)] public int Age { get; set; } + + public override string ToString() + { + return $"Name = {Name}, Age = {Age}"; + } } } diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs new file mode 100644 index 0000000000..0986986da0 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class PolymorphicBinding : PageModel + { + [ModelBinder(typeof(PolymorphicModelBinder))] + public IUserModel UserModel { get; set; } + + public IActionResult OnPost() + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return new ContentResult { Content = UserModel.ToString() }; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml new file mode 100644 index 0000000000..7bde2525de --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PolymorphicBinding.cshtml @@ -0,0 +1,6 @@ +@page +@model PolymorphicBinding + +
+ @Html.AntiForgeryToken() +
\ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs b/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs new file mode 100644 index 0000000000..bce3d79b8a --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/PolymorphicModelBinder.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace RazorPagesWebSite +{ + public class PolymorphicModelBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var ageValue = bindingContext.ValueProvider.GetValue(nameof(UserModel.Age)); + var age = 0; + if (ageValue.Length != 0) + { + age = int.Parse(ageValue.FirstValue); + } + + + var model = new UserModel + { + Name = bindingContext.ValueProvider.GetValue(nameof(UserModel.Name)).FirstValue, + Age = age, + }; + + bindingContext.Result = ModelBindingResult.Success(model); + + return Task.CompletedTask; + } + } +}