diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs index 458502d802..5f7daf43f9 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs @@ -4,4 +4,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 3de7a91ebe..4a11189cb2 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using Xunit; @@ -420,14 +419,16 @@ Hello from /Pages/Shared/"; var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); - var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); - message.Content = new FormUrlEncodedContent(new Dictionary + var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel") { - ["__RequestVerificationToken"] = token, - ["ConfirmPassword"] = "", - ["Password"] = "", - ["Email"] = "" - }); + Content = new FormUrlEncodedContent(new Dictionary + { + ["__RequestVerificationToken"] = token, + ["ConfirmPassword"] = "", + ["Password"] = "", + ["Email"] = "" + }) + }; message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); // Act @@ -443,18 +444,20 @@ Hello from /Pages/Shared/"; public async Task PageConventions_CustomizedModelCanWorkWithModelState() { // Arrange - var getPage = await Client.GetAsync("/CustomModelTypeModel"); + var getPage = await Client.GetAsync("/CustomModelTypeModel?Attempts=0"); var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); - var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); - message.Content = new FormUrlEncodedContent(new Dictionary + var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel?Attempts=3") { - ["__RequestVerificationToken"] = token, - ["Email"] = "javi@example.com", - ["Password"] = "Password.12$", - ["ConfirmPassword"] = "Password.12$", - }); + Content = new FormUrlEncodedContent(new Dictionary + { + ["__RequestVerificationToken"] = token, + ["Email"] = "javi@example.com", + ["Password"] = "Password.12$", + ["ConfirmPassword"] = "Password.12$", + }) + }; message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); // Act @@ -465,6 +468,37 @@ Hello from /Pages/Shared/"; Assert.Equal("/", response.Headers.Location.ToString()); } + [Fact] + public async Task PageConventions_CustomizedModelCanWorkWithModelState_EnforcesBindRequired() + { + // Arrange + var getPage = await Client.GetAsync("/CustomModelTypeModel?Attempts=0"); + var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); + var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); + + var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel") + { + Content = new FormUrlEncodedContent(new Dictionary + { + ["__RequestVerificationToken"] = token, + ["Email"] = "javi@example.com", + ["Password"] = "Password.12$", + ["ConfirmPassword"] = "Password.12$", + }) + }; + message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Contains( + "A value for the 'Attempts' parameter or property was not provided.", + responseText); + } + [Fact] public async Task ValidationAttributes_OnTopLevelProperties() { @@ -642,10 +676,12 @@ Hello from /Pages/Shared/"; var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response); - var content = new MultipartFormDataContent(); - content.Add(new StringContent("property1-value"), property1); - content.Add(new StringContent("test-value1"), file1, "test1.txt"); - content.Add(new StringContent("test-value2"), file3, "test2.txt"); + var content = new MultipartFormDataContent + { + { new StringContent("property1-value"), property1 }, + { new StringContent("test-value1"), file1, "test1.txt" }, + { new StringContent("test-value2"), file3, "test2.txt" } + }; var request = new HttpRequestMessage(HttpMethod.Post, url) { diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindPropertyIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindPropertyIntegrationTest.cs index 1b42c219f0..d054633f72 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindPropertyIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindPropertyIntegrationTest.cs @@ -1,15 +1,17 @@ // 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.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Xunit; namespace Microsoft.AspNetCore.Mvc.IntegrationTests @@ -179,6 +181,74 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } } + [Theory] + [InlineData(null, false)] + [InlineData(123, true)] + public async Task BindModelAsync_WithBindPageProperty_EnforcesBindRequired(int? input, bool isValid) + { + // Arrange + var propertyInfo = typeof(TestPage).GetProperty(nameof(TestPage.BindRequiredProperty)); + var propertyDescriptor = new PageBoundPropertyDescriptor + { + BindingInfo = BindingInfo.GetBindingInfo(new[] + { + new FromQueryAttribute { Name = propertyInfo.Name }, + }), + Name = propertyInfo.Name, + ParameterType = propertyInfo.PropertyType, + Property = propertyInfo, + }; + + var typeInfo = typeof(TestPage).GetTypeInfo(); + var actionDescriptor = new CompiledPageActionDescriptor + { + BoundProperties = new[] { propertyDescriptor }, + HandlerTypeInfo = typeInfo, + ModelTypeInfo = typeInfo, + PageTypeInfo = typeInfo, + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.Method = "POST"; + if (input.HasValue) + { + request.QueryString = new QueryString($"?{propertyDescriptor.Name}={input.Value}"); + } + }); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider); + var modelBinderFactory = ModelBindingTestHelper.GetModelBinderFactory(modelMetadataProvider); + var modelMetadata = modelMetadataProvider + .GetMetadataForProperty(typeof(TestPage), propertyDescriptor.Name); + + var pageBinder = PageBinderFactory.CreatePropertyBinder( + parameterBinder, + modelMetadataProvider, + modelBinderFactory, + actionDescriptor); + var pageContext = new PageContext + { + ActionDescriptor = actionDescriptor, + HttpContext = testContext.HttpContext, + RouteData = testContext.RouteData, + ValueProviderFactories = testContext.ValueProviderFactories, + }; + + var page = new TestPage(); + + // Act + await pageBinder(pageContext, page); + + // Assert + Assert.Equal(isValid, pageContext.ModelState.IsValid); + if (isValid) + { + Assert.Equal(input.Value, page.BindRequiredProperty); + } + } + [Theory] [InlineData("RequiredAndStringLengthProp", null, false)] [InlineData("RequiredAndStringLengthProp", "", false)] @@ -231,12 +301,18 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } } - class TestController + private class TestController { [BindNever] public string BindNeverProp { get; set; } [BindRequired] public int BindRequiredProp { get; set; } [Required, StringLength(3)] public string RequiredAndStringLengthProp { get; set; } [DisplayName("My Display Name"), StringLength(3)] public string DisplayNameStringLengthProp { get; set; } } + + private class TestPage : PageModel + { + [BindRequired] + public int BindRequiredProperty { get; set; } + } } } diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs index e32a2e83c4..e78cce0428 100644 --- a/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; @@ -13,6 +14,10 @@ namespace RazorPagesWebSite public string ReturnUrl { get; set; } + [BindRequired] + [FromQuery(Name = nameof(Attempts))] + public int Attempts { get; set; } + public class InputModel { [Required] @@ -69,10 +74,13 @@ namespace RazorPagesWebSite { if (!ModelState.IsValid) { + Attempts++; + RouteData.Values.Add(nameof(Attempts), Attempts); + return Page(); } return Redirect("~/"); } } -} \ No newline at end of file +}