diff --git a/src/Microsoft.AspNetCore.Mvc.Core/BindPropertyAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/BindPropertyAttribute.cs index 4c12dd33d5..b36b79444f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/BindPropertyAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/BindPropertyAttribute.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc { - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata, IRequestPredicateProvider { private static readonly Func _supportsAllRequests = (c) => true; @@ -31,22 +31,14 @@ namespace Microsoft.AspNetCore.Mvc return _bindingSource; } - protected set - { - _bindingSource = value; - } + protected set => _bindingSource = value; } /// public string Name { get; set; } Func IRequestPredicateProvider.RequestPredicate - { - get - { - return SupportsGet ? _supportsAllRequests : _supportsNonGetRequests; - } - } + => SupportsGet ? _supportsAllRequests : _supportsNonGetRequests; private static bool IsNonGetRequest(ActionContext context) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs index 49f0417615..de190cca16 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs @@ -224,7 +224,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal return propertyModel; } - /// /// Creates the instance for the given action . /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs index a844d59b40..1dae7a10da 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs @@ -220,10 +220,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal throw new ArgumentNullException(nameof(property)); } - var attributes = property.GetCustomAttributes(inherit: true); - var bindingInfo = BindingInfo.GetBindingInfo(attributes); + var propertyAttributes = property.GetCustomAttributes(inherit: true); + var handlerAttributes = property.DeclaringType.GetCustomAttributes(inherit: true); - var model = new PagePropertyModel(property, property.GetCustomAttributes(inherit: true)) + // Look for binding info on the handler if nothing is specified on the property. + // This allows BindProperty attributes on handlers to apply to properties. + var bindingInfo = BindingInfo.GetBindingInfo(propertyAttributes) ?? + BindingInfo.GetBindingInfo(handlerAttributes); + + var model = new PagePropertyModel(property, propertyAttributes) { PropertyName = property.Name, BindingInfo = bindingInfo, diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index f94fb8dc99..c170620079 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1136,9 +1136,59 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP // Act var response = await Client.GetStringAsync("/Pages/Localized/PageWithModel?culture=fr-FR"); + // Assert Assert.Equal(expected, response.Trim()); } + [Fact] + public async Task BindPropertyAttribute_CanBeAppliedToModelType() + { + // Arrange + var expected = "Property1 = 123, Property2 = 25,"; + var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123") + { + Content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("Property2", "25"), + }), + }; + await AddAntiforgeryHeaders(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.StartsWith(expected, responseContent.Trim()); + } + + [Fact] + public async Task BindingInfoOnPropertiesIsPreferredToBindingInfoOnType() + { + // Arrange + var expected = "Property1 = 123, Property2 = 25,"; + var request = new HttpRequestMessage(HttpMethod.Post, "/Pages/PropertyBinding/BindPropertyOnModel?Property1=123") + { + Content = new FormUrlEncodedContent(new[] + { + // FormValueProvider appears before QueryStringValueProvider. However, the FromQuery explicitly listed + // on the property should cause it to use the latter. + new KeyValuePair("Property1", "345"), + new KeyValuePair("Property2", "25"), + }), + }; + await AddAntiforgeryHeaders(request); + + // Act + var response = await Client.SendAsync(request); + + // Assert + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.StartsWith(expected, responseContent.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs index c01a36c1f7..3bf8b5f2ed 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; -using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -229,6 +228,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public string Property { get; set; } } + [Fact] + public void OnProvidersExecuting_DiscoversProperties_FromAllSubTypesThatDeclaresBindProperty() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(BindPropertyAttributeOnBaseModelPage).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName).Where(p => p.BindingInfo != null), + property => + { + var name = nameof(ModelLevel3.Property2); + Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo); + Assert.Equal(name, property.PropertyName); + Assert.NotNull(property.BindingInfo); + }, + property => + { + var name = nameof(ModelLevel3.Property3); + Assert.Equal(typeof(ModelLevel3).GetProperty(name), property.PropertyInfo); + Assert.Equal(name, property.PropertyName); + Assert.NotNull(property.BindingInfo); + }); + } + + private class BindPropertyAttributeOnBaseModelPage : Page + { + public ModelLevel3 Model => null; + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + private class ModelLevel1 : PageModel + { + public string Property1 { get; set; } + } + + [BindProperty] + private class ModelLevel2 : ModelLevel1 + { + public string Property2 { get; set; } + } + + private class ModelLevel3 : ModelLevel2 + { + public string Property3 { get; set; } + } + [Fact] public void OnProvidersExecuting_DiscoversHandlersFromPage() { @@ -305,6 +358,53 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); } + [Fact] + public void OnProvidersExecuting_DiscoversBindingInfoFromHandler() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(PageWithBindPropertyModel).GetTypeInfo(); + var modelType = typeof(ModelWithBindProperty); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName), + property => + { + Assert.Equal(nameof(ModelWithBindProperty.Property1), property.PropertyName); + Assert.NotNull(property.BindingInfo); + }, + property => + { + Assert.Equal(nameof(ModelWithBindProperty.Property2), property.PropertyName); + Assert.NotNull(property.BindingInfo); + Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource); + }); + } + + private class PageWithBindPropertyModel : PageBase + { + public ModelWithBindProperty Model => null; + + public override Task ExecuteAsync() => null; + } + + [BindProperty] + [PageModel] + private class ModelWithBindProperty + { + public string Property1 { get; set; } + + [FromRoute] + public string Property2 { get; set; } + } + [Fact] public void OnProvidersExecuting_DiscoversHandlersFromModel() { diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cs b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cs new file mode 100644 index 0000000000..4972e2a653 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cs @@ -0,0 +1,18 @@ +// 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.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + [BindProperty] + public class BindPropertyOnModel : PageModel + { + [FromQuery] + public string Property1 { get; set; } + + public string Property2 { get; set; } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cshtml new file mode 100644 index 0000000000..0c0fc39cd6 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyOnModel.cshtml @@ -0,0 +1,8 @@ +@page +@model BindPropertyOnModel + +Property1 = @Model.Property1, Property2 = @Model.Property2, + +
+ @Html.AntiForgeryToken() +