From 06e40252a057c88c95f05f4668c192e473f92b1f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 13 Feb 2018 22:34:27 -0800 Subject: [PATCH] Add support for top level validation to Razor Pages --- .../ControllerBinderDelegateProvider.cs | 17 +- .../CompiledPageRouteModelProvider.cs | 1 - .../Internal/ExecutorFactory.cs | 2 +- .../Internal/PageActionInvoker.cs | 33 +- .../Internal/PageActionInvokerCacheEntry.cs | 11 +- .../Internal/PageActionInvokerProvider.cs | 40 +- .../Internal/PageBinderFactory.cs | 177 +++++++++ .../Internal/PageHandlerBinderDelegate.cs | 10 + .../Internal/PageHandlerExecutorDelegate.cs | 9 + .../Internal/PagePropertyBinderFactory.cs | 69 ---- .../RazorPagesWithBasePathTest.cs | 21 + .../Internal/PageActionInvokerProviderTest.cs | 2 + .../Internal/PageActionInvokerTest.cs | 10 +- ...actoryTest.cs => PageBinderFactoryTest.cs} | 366 ++++++++++++++++-- .../PageHandlerWithValidation.cshtml | 10 + .../Pages/Validation/PageWithValidation.cs | 17 + .../Validation/PageWithValidation.cshtml | 3 + .../Pages/Validation/_ViewImports.cshtml | 1 + 18 files changed, 663 insertions(+), 136 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageBinderFactory.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerBinderDelegate.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerExecutorDelegate.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs rename test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/{PagePropertyBinderFactoryTest.cs => PageBinderFactoryTest.cs} (60%) create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Validation/PageHandlerWithValidation.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Validation/_ViewImports.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerBinderDelegateProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerBinderDelegateProvider.cs index 06c493b79c..a00265b24e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerBinderDelegateProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerBinderDelegateProvider.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc.Internal { + // Note: changes made to binding behavior in type should also be made to PageBinderFactory. public static class ControllerBinderDelegateProvider { public static ControllerBinderDelegate CreateBinderDelegate( @@ -100,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } - private static BindingInfo[] GetParameterBindingInfo( + private static BinderItem[] GetParameterBindingInfo( IModelBinderFactory modelBinderFactory, IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor) @@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal return null; } - var parameterBindingInfo = new BindingInfo[parameters.Count]; + var parameterBindingInfo = new BinderItem[parameters.Count]; for (var i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; @@ -140,13 +141,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal CacheToken = parameter, }); - parameterBindingInfo[i] = new BindingInfo(binder, metadata); + parameterBindingInfo[i] = new BinderItem(binder, metadata); } return parameterBindingInfo; } - private static BindingInfo[] GetPropertyBindingInfo( + private static BinderItem[] GetPropertyBindingInfo( IModelBinderFactory modelBinderFactory, IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor) @@ -157,7 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal return null; } - var propertyBindingInfo = new BindingInfo[properties.Count]; + var propertyBindingInfo = new BinderItem[properties.Count]; var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); for (var i = 0; i < properties.Count; i++) { @@ -170,15 +171,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal CacheToken = property, }); - propertyBindingInfo[i] = new BindingInfo(binder, metadata); + propertyBindingInfo[i] = new BinderItem(binder, metadata); } return propertyBindingInfo; } - private struct BindingInfo + private struct BinderItem { - public BindingInfo(IModelBinder modelBinder, ModelMetadata modelMetadata) + public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata) { ModelBinder = modelBinder; ModelMetadata = modelMetadata; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs index 5e9cd2d38c..d31163d58e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs index 5a96ec5795..6bea1e9605 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public static class ExecutorFactory { - public static Func> CreateExecutor(HandlerMethodDescriptor handlerDescriptor) + public static PageHandlerExecutorDelegate CreateExecutor(HandlerMethodDescriptor handlerDescriptor) { if (handlerDescriptor == null) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 0a8396a997..49eb71c523 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -154,33 +154,26 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private async Task BindArgumentsCoreAsync() { - if (CacheEntry.PropertyBinder != null) - { - await CacheEntry.PropertyBinder(_pageContext, _instance); - } + await CacheEntry.PropertyBinder(_pageContext, _instance); if (_handler == null) { return; } - var valueProvider = await CompositeValueProvider.CreateAsync(_pageContext, _pageContext.ValueProviderFactories); - - for (var i = 0; i < _handler.Parameters.Count; i++) + // We do two separate cache lookups, once for the binder and once for the executor. + // Reduding it to a single lookup requires a lot of code change with little value. + PageHandlerBinderDelegate handlerBinder = null; + for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++) { - var parameter = _handler.Parameters[i]; - - var result = await _parameterBinder.BindModelAsync( - _pageContext, - valueProvider, - parameter, - value: null); - - if (result.IsModelSet) + if (object.ReferenceEquals(_handler, _actionDescriptor.HandlerMethods[i])) { - _arguments[parameter.Name] = result.Model; + handlerBinder = CacheEntry.HandlerBinders[i]; + break; } } + + await handlerBinder(_pageContext, _arguments); } private static object[] PrepareArguments( @@ -223,16 +216,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var arguments = PrepareArguments(_arguments, handler); - Func> executor = null; + PageHandlerExecutorDelegate executor = null; for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++) { if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i])) { - executor = CacheEntry.Executors[i]; + executor = CacheEntry.HandlerExecutors[i]; break; } } + Debug.Assert(executor != null, "We should always find a executor for a handler"); + _diagnosticSource.BeforeHandlerMethod(_pageContext, handler, _arguments, _instance); _logger.ExecutingHandlerMethod(_pageContext, handler, arguments); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs index e3a49385fe..f2844d2023 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs @@ -22,7 +22,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Func modelFactory, Action releaseModel, Func propertyBinder, - Func>[] executors, + PageHandlerExecutorDelegate[] handlerExecutors, + PageHandlerBinderDelegate[] handlerBinders, IReadOnlyList> viewStartFactories, FilterItem[] cacheableFilters) { @@ -33,7 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ModelFactory = modelFactory; ReleaseModel = releaseModel; PropertyBinder = propertyBinder; - Executors = executors; + HandlerExecutors = handlerExecutors; + HandlerBinders = handlerBinders; ViewStartFactories = viewStartFactories; CacheableFilters = cacheableFilters; } @@ -60,7 +62,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal /// public Func PropertyBinder { get; } - public Func>[] Executors { get; } + public PageHandlerExecutorDelegate[] HandlerExecutors { get; } + + public PageHandlerBinderDelegate[] HandlerBinders { get; } public Func ViewDataFactory { get; } @@ -70,5 +74,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public IReadOnlyList> ViewStartFactories { get; } public FilterItem[] CacheableFilters { get; } + } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 3c4917abf2..04ad0dcc51 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private readonly IPageLoader _loader; private readonly IPageFactoryProvider _pageFactoryProvider; private readonly IPageModelFactoryProvider _modelFactoryProvider; + private readonly IModelBinderFactory _modelBinderFactory; private readonly IRazorPageFactoryProvider _razorPageFactoryProvider; private readonly IActionDescriptorCollectionProvider _collectionProvider; private readonly IFilterProvider[] _filterProviders; @@ -52,6 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal IEnumerable filterProviders, ParameterBinder parameterBinder, IModelMetadataProvider modelMetadataProvider, + IModelBinderFactory modelBinderFactory, ITempDataDictionaryFactory tempDataFactory, IOptions mvcOptions, IOptions htmlHelperOptions, @@ -63,6 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _loader = loader; _pageFactoryProvider = pageFactoryProvider; _modelFactoryProvider = modelFactoryProvider; + _modelBinderFactory = modelBinderFactory; _razorPageFactoryProvider = razorPageFactoryProvider; _collectionProvider = collectionProvider; _filterProviders = filterProviders.ToArray(); @@ -172,9 +175,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageFactory = _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor); var pageDisposer = _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor); - var propertyBinder = PagePropertyBinderFactory.CreateBinder( + var propertyBinder = PageBinderFactory.CreatePropertyBinder( _parameterBinder, _modelMetadataProvider, + _modelBinderFactory, compiledActionDescriptor); Func modelFactory = null; @@ -187,7 +191,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var viewStartFactories = GetViewStartFactories(compiledActionDescriptor); - var executors = GetExecutors(compiledActionDescriptor); + var handlerExecutors = GetHandlerExecutors(compiledActionDescriptor); + var handlerBinders = GetHandlerBinders(compiledActionDescriptor); return new PageActionInvokerCacheEntry( compiledActionDescriptor, @@ -197,7 +202,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal modelFactory, modelReleaser, propertyBinder, - executors, + handlerExecutors, + handlerBinders, viewStartFactories, cachedFilters); } @@ -222,14 +228,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return viewStartFactories; } - private static Func>[] GetExecutors(CompiledPageActionDescriptor actionDescriptor) + private static PageHandlerExecutorDelegate[] GetHandlerExecutors(CompiledPageActionDescriptor actionDescriptor) { if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0) { - return Array.Empty>>(); + return Array.Empty(); } - var results = new Func>[actionDescriptor.HandlerMethods.Count]; + var results = new PageHandlerExecutorDelegate[actionDescriptor.HandlerMethods.Count]; for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++) { @@ -239,6 +245,28 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return results; } + private PageHandlerBinderDelegate[] GetHandlerBinders(CompiledPageActionDescriptor actionDescriptor) + { + if (actionDescriptor.HandlerMethods == null ||actionDescriptor.HandlerMethods.Count == 0) + { + return Array.Empty(); + } + + var results = new PageHandlerBinderDelegate[actionDescriptor.HandlerMethods.Count]; + + for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++) + { + results[i] = PageBinderFactory.CreateHandlerBinder( + _parameterBinder, + _modelMetadataProvider, + _modelBinderFactory, + actionDescriptor, + actionDescriptor.HandlerMethods[i]); + } + + return results; + } + internal class InnerCache { public InnerCache(int version) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageBinderFactory.cs new file mode 100644 index 0000000000..0b5900dbe4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageBinderFactory.cs @@ -0,0 +1,177 @@ +// 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 System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + internal static class PageBinderFactory + { + internal static readonly Func NullPropertyBinder = (context, arguments) => Task.CompletedTask; + internal static readonly PageHandlerBinderDelegate NullHandlerBinder = (context, arguments) => Task.CompletedTask; + + public static Func CreatePropertyBinder( + ParameterBinder parameterBinder, + IModelMetadataProvider modelMetadataProvider, + IModelBinderFactory modelBinderFactory, + CompiledPageActionDescriptor actionDescriptor) + { + if (parameterBinder == null) + { + throw new ArgumentNullException(nameof(parameterBinder)); + } + + if (actionDescriptor == null) + { + throw new ArgumentNullException(nameof(actionDescriptor)); + } + + var properties = actionDescriptor.BoundProperties; + if (properties == null || properties.Count == 0) + { + return NullPropertyBinder; + } + + var handlerType = actionDescriptor.HandlerTypeInfo.AsType(); + var propertyBindingInfo = new BinderItem[properties.Count]; + for (var i = 0; i < properties.Count; i++) + { + var property = properties[i]; + var metadata = modelMetadataProvider.GetMetadataForProperty(handlerType, property.Name); + var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext + { + BindingInfo = property.BindingInfo, + Metadata = metadata, + CacheToken = property, + }); + + propertyBindingInfo[i] = new BinderItem(binder, metadata); + } + + return Bind; + + async Task Bind(PageContext pageContext, object instance) + { + var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); + for (var i = 0; i < properties.Count; i++) + { + var property = properties[i]; + var bindingInfo = propertyBindingInfo[i]; + var modelMetadata = bindingInfo.ModelMetadata; + + if (!modelMetadata.IsBindingAllowed) + { + continue; + } + + var result = await parameterBinder.BindModelAsync( + pageContext, + bindingInfo.ModelBinder, + valueProvider, + property, + modelMetadata, + value: null); + + if (result.IsModelSet) + { + PropertyValueSetter.SetValue(bindingInfo.ModelMetadata, instance, result.Model); + } + } + } + } + + public static PageHandlerBinderDelegate CreateHandlerBinder( + ParameterBinder parameterBinder, + IModelMetadataProvider modelMetadataProvider, + IModelBinderFactory modelBinderFactory, + CompiledPageActionDescriptor actionDescriptor, + HandlerMethodDescriptor handler) + { + if (handler.Parameters == null || handler.Parameters.Count == 0) + { + return NullHandlerBinder; + } + + var handlerType = actionDescriptor.HandlerTypeInfo.AsType(); + var parameterBindingInfo = new BinderItem[handler.Parameters.Count]; + for (var i = 0; i < parameterBindingInfo.Length; i++) + { + var parameter = handler.Parameters[i]; + ModelMetadata metadata; + if (modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) + { + // The default model metadata provider derives from ModelMetadataProvider + // and can therefore supply information about attributes applied to parameters. + metadata = modelMetadataProviderBase.GetMetadataForParameter(parameter.ParameterInfo); + } + else + { + // For backward compatibility, if there's a custom model metadata provider that + // only implements the older IModelMetadataProvider interface, access the more + // limited metadata information it supplies. In this scenario, validation attributes + // are not supported on parameters. + metadata = modelMetadataProvider.GetMetadataForType(parameter.ParameterType); + } + + var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext + { + BindingInfo = parameter.BindingInfo, + Metadata = metadata, + CacheToken = parameter, + }); + + parameterBindingInfo[i] = new BinderItem(binder, metadata); + } + + return Bind; + + async Task Bind(PageContext pageContext, IDictionary arguments) + { + var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); + + for (var i = 0; i < parameterBindingInfo.Length; i++) + { + var parameter = handler.Parameters[i]; + var bindingInfo = parameterBindingInfo[i]; + var modelMetadata = bindingInfo.ModelMetadata; + + if (!modelMetadata.IsBindingAllowed) + { + continue; + } + + var result = await parameterBinder.BindModelAsync( + pageContext, + bindingInfo.ModelBinder, + valueProvider, + parameter, + modelMetadata, + value: null); + + if (result.IsModelSet) + { + arguments[parameter.Name] = result.Model; + } + } + } + } + + private struct BinderItem + { + public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata) + { + ModelMetadata = modelMetadata; + ModelBinder = modelBinder; + } + + public ModelMetadata ModelMetadata { get; } + + public IModelBinder ModelBinder { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerBinderDelegate.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerBinderDelegate.cs new file mode 100644 index 0000000000..7bea47fc51 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerBinderDelegate.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. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public delegate Task PageHandlerBinderDelegate(PageContext pageContext, IDictionary arguments); +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerExecutorDelegate.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerExecutorDelegate.cs new file mode 100644 index 0000000000..e90c24dc75 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerExecutorDelegate.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. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public delegate Task PageHandlerExecutorDelegate(object handler, object[] arguments); +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs deleted file mode 100644 index 3d6a895e47..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs +++ /dev/null @@ -1,69 +0,0 @@ -// 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 System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal -{ - public static class PagePropertyBinderFactory - { - public static Func CreateBinder( - ParameterBinder parameterBinder, - IModelMetadataProvider modelMetadataProvider, - CompiledPageActionDescriptor actionDescriptor) - { - if (parameterBinder == null) - { - throw new ArgumentNullException(nameof(parameterBinder)); - } - - if (actionDescriptor == null) - { - throw new ArgumentNullException(nameof(actionDescriptor)); - } - - var properties = actionDescriptor.BoundProperties; - if (properties == null || properties.Count == 0) - { - return null; - } - - var type = actionDescriptor.HandlerTypeInfo.AsType(); - var metadata = new ModelMetadata[properties.Count]; - for (var i = 0; i < properties.Count; i++) - { - metadata[i] = modelMetadataProvider.GetMetadataForProperty(type, properties[i].Name); - } - - return Bind; - - Task Bind(PageContext pageContext, object instance) - { - return BindPropertiesAsync(parameterBinder, pageContext, instance, properties, metadata); - } - } - - private static async Task BindPropertiesAsync( - ParameterBinder parameterBinder, - PageContext pageContext, - object instance, - IList properties, - IList metadata) - { - var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); - for (var i = 0; i < properties.Count; i++) - { - var result = await parameterBinder.BindModelAsync(pageContext, valueProvider, properties[i]); - if (result.IsModelSet) - { - PropertyValueSetter.SetValue(metadata[i], instance, result.Model); - } - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 6868c14512..c5d97817cb 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -463,5 +463,26 @@ Hello from /Pages/Shared/"; Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.ToString()); } + + [Fact] + public async Task ValidationAttributes_OnTopLevelProperties() + { + // Act + var response = await Client.GetStringAsync("/Validation/PageWithValidation?age=71"); + + // Assert + Assert.Contains("Name is required", response); + Assert.Contains("18 ≤ Age ≤ 60", response); + } + + [Fact] + public async Task ValidationAttributes_OnHandlerParameters() + { + // Act + var response = await Client.GetStringAsync("/Validation/PageHandlerWithValidation"); + + // Assert + Assert.Contains("Name is required", response); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 02e3086e98..a63b35257c 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -486,6 +486,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); var parameterBinder = new ParameterBinder( modelMetadataProvider, TestModelBinderFactory.CreateDefault(), @@ -501,6 +502,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal new IFilterProvider[0], parameterBinder, modelMetadataProvider, + modelBinderFactory, tempDataFactory.Object, Options.Create(new MvcOptions()), Options.Create(new HtmlHelperOptions()), diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs index eecbd3c7fb..b3bb51a9d1 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs @@ -1102,7 +1102,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal BoundProperties = new List(), }; - var handlers = new List>>(); + var handlers = new List(); if (result != null) { handlers.Add((obj, args) => Task.FromResult(result)); @@ -1135,7 +1135,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Func modelFactory = null, ITempDataDictionaryFactory tempDataFactory = null, IList valueProviderFactories = null, - Func>[] handlers = null, + PageHandlerExecutorDelegate[] handlers = null, + PageHandlerBinderDelegate[] handlerBinders = null, RouteData routeData = null, ILogger logger = null, TestDiagnosticListener listener = null) @@ -1178,13 +1179,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal if (handlers == null) { - handlers = new Func>[actionDescriptor.HandlerMethods.Count]; + handlers = new PageHandlerExecutorDelegate[actionDescriptor.HandlerMethods.Count]; for (var i = 0; i < handlers.Length; i++) { handlers[i] = (obj, args) => Task.FromResult(new PageResult()); } } + handlerBinders = handlerBinders ?? Array.Empty(); + if (modelFactory == null) { modelFactory = _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType()); @@ -1199,6 +1202,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal (c, model) => { (model as IDisposable)?.Dispose(); }, null, handlers, + handlerBinders, null, new FilterItem[0]); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageBinderFactoryTest.cs similarity index 60% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageBinderFactoryTest.cs index 7b970b6efe..7c1bd73036 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageBinderFactoryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -18,7 +19,7 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class PagePropertyBinderFactoryTest + public class PageBinderFactoryTest { [Fact] public void GetModelBinderFactory_ReturnsNullIfPageHasNoBoundProperties() @@ -29,17 +30,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal PageTypeInfo = typeof(PageWithNoBoundProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var binder = new ParameterBinder( modelMetadataProvider, - TestModelBinderFactory.CreateDefault(), + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -52,18 +55,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ModelTypeInfo = typeof(PageModelWithNoBoundProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); var binder = new ParameterBinder( - TestModelMetadataProvider.CreateDefaultProvider(), - TestModelBinderFactory.CreateDefault(), + modelMetadataProvider, + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -75,17 +79,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal PageTypeInfo = typeof(PageWithNoVisibleBoundProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var binder = new ParameterBinder( modelMetadataProvider, - TestModelBinderFactory.CreateDefault(), + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -98,17 +104,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ModelTypeInfo = typeof(PageModelWithNoVisibleBoundProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var binder = new ParameterBinder( modelMetadataProvider, - TestModelBinderFactory.CreateDefault(), + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -120,17 +128,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal PageTypeInfo = typeof(PageWithReadOnlyProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var binder = new ParameterBinder( modelMetadataProvider, - TestModelBinderFactory.CreateDefault(), + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -143,17 +153,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ModelTypeInfo = typeof(PageModelWithReadOnlyProperties).GetTypeInfo(), }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var binder = new ParameterBinder( modelMetadataProvider, - TestModelBinderFactory.CreateDefault(), + modelBinderFactory, Mock.Of(), NullLoggerFactory.Instance); // Act - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); // Assert - Assert.Null(factory); + Assert.Same(PageBinderFactory.NullPropertyBinder, factory); } [Fact] @@ -190,6 +202,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); var binder = new TestParameterBinder(new Dictionary { @@ -197,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { nameof(PageWithProperty.RouteDifferentValue), "route-value" } }); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); var page = new PageWithProperty { @@ -255,7 +268,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); var page = new PageWithProperty { @@ -303,7 +318,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var binder = new TestParameterBinder(new Dictionary()); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); var page = new PageWithProperty { @@ -364,7 +381,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); var page = new PageWithProperty { @@ -421,7 +440,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); var page = new PageWithProperty { @@ -440,6 +461,264 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal("value", model.Default); } + [Fact] + public async Task CreatePropertyBinder_SkipsBindingPropertiesWithBindNever() + { + // Arrange + var type = typeof(PageModelWithBindNeverProperty).GetTypeInfo(); + + var actionDescriptor = new CompiledPageActionDescriptor + { + BoundProperties = new[] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithBindNeverProperty.BindNeverProperty), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithBindNeverProperty.BindNeverProperty)), + }, + }, + + HandlerTypeInfo = type, + PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), + ModelTypeInfo = type, + }; + + var binder = new TestParameterBinder(new Dictionary + { + { nameof(PageModelWithBindNeverProperty.BindNeverProperty), "value" }, + }); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); + + var page = new PageWithProperty + { + PageContext = GetPageContext() + }; + + var model = new PageModelWithBindNeverProperty(); + + // Act + await factory(page.PageContext, model); + + // Assert + Assert.Null(model.BindNeverProperty); + } + + [Fact] + public async Task CreatePropertyBinder_ValidatesTopLevelProperties() + { + // Arrange + var type = typeof(PageModelWithValidation).GetTypeInfo(); + + var actionDescriptor = new CompiledPageActionDescriptor + { + BoundProperties = new[] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithValidation.Validated), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithValidation.Validated)), + }, + }, + + HandlerTypeInfo = type, + PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), + ModelTypeInfo = type, + }; + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); + + var binder = new ParameterBinder( + modelMetadataProvider, + modelBinderFactory, + validatorProvider, + NullLoggerFactory.Instance); + + var factory = PageBinderFactory.CreatePropertyBinder(binder, modelMetadataProvider, modelBinderFactory, actionDescriptor); + + var page = new PageWithProperty + { + PageContext = GetPageContext() + }; + + var model = new PageModelWithValidation(); + + // Act + await factory(page.PageContext, model); + + // Assert + var modelState = page.PageContext.ModelState; + Assert.False(modelState.IsValid); + Assert.Collection( + modelState, + kvp => + { + Assert.Equal(nameof(PageModelWithValidation.Validated), kvp.Key); + }); + } + + [Fact] + public async Task CreateHandlerBinder_BindsHandlerParameters() + { + // Arrange + var type = typeof(PageModelWithExecutors); + var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGet)); + + // Act + var parameterBinder = new TestParameterBinder(new Dictionary() + { + { "id", "value" }, + }); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreateHandlerBinder( + parameterBinder, + modelMetadataProvider, + modelBinderFactory, + actionDescriptor, + actionDescriptor.HandlerMethods[0]); + + var page = new PageWithProperty + { + PageContext = GetPageContext() + }; + + var model = new PageModelWithExecutors(); + var arguments = new Dictionary(); + + // Act + await factory(page.PageContext, arguments); + + // Assert + Assert.Collection( + arguments, + kvp => + { + Assert.Equal("id", kvp.Key); + Assert.Equal("value", kvp.Value); + }); + } + + [Fact] + public async Task CreateHandlerBinder_SkipBindingParametersThatDisallowBinding() + { + // Arrange + var type = typeof(PageModelWithExecutors); + var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGetWithBindNever)); + + // Act + var parameterBinder = new TestParameterBinder(new Dictionary() + { + { "id", "value" }, + }); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + + var factory = PageBinderFactory.CreateHandlerBinder( + parameterBinder, + modelMetadataProvider, + modelBinderFactory, + actionDescriptor, + actionDescriptor.HandlerMethods[0]); + + var page = new PageWithProperty + { + PageContext = GetPageContext() + }; + + var model = new PageModelWithExecutors(); + var arguments = new Dictionary(); + + // Act + await factory(page.PageContext, arguments); + + // Assert + Assert.Empty(arguments); + } + + [Fact] + public async Task CreateHandlerBinder_ValidatesTopLevelParameters() + { + // Arrange + var type = typeof(PageModelWithExecutors); + var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnPostWithValidation)); + + // Act + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var modelBinderFactory = TestModelBinderFactory.CreateDefault(); + var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); + + var parameterBinder = new ParameterBinder(modelMetadataProvider, modelBinderFactory, validatorProvider, NullLoggerFactory.Instance); + + var factory = PageBinderFactory.CreateHandlerBinder( + parameterBinder, + modelMetadataProvider, + modelBinderFactory, + actionDescriptor, + actionDescriptor.HandlerMethods[0]); + + var page = new PageWithProperty + { + PageContext = GetPageContext() + }; + + var model = new PageModelWithExecutors(); + var arguments = new Dictionary(); + + // Act + await factory(page.PageContext, arguments); + + // Assert + var modelState = page.PageContext.ModelState; + Assert.False(modelState.IsValid); + Assert.Collection( + page.PageContext.ModelState, + kvp => + { + Assert.Equal("name", kvp.Key); + }); + } + + private static CompiledPageActionDescriptor GetActionDescriptorWithHandlerMethod(Type type, string method) + { + var handlerMethodInfo = type.GetMethod(method); + var parameterInfo = handlerMethodInfo.GetParameters()[0]; + + var actionDescriptor = new CompiledPageActionDescriptor + { + HandlerTypeInfo = type.GetTypeInfo(), + HandlerMethods = new[] + { + new HandlerMethodDescriptor + { + HttpMethod = "Post", + MethodInfo = handlerMethodInfo, + Parameters = new[] + { + new HandlerParameterDescriptor + { + ParameterInfo = parameterInfo, + ParameterType = parameterInfo.ParameterType, + Name = parameterInfo.Name + }, + }, + }, + }, + }; + return actionDescriptor; + } + private PageContext GetPageContext(string httpMethod = null) { var services = new ServiceCollection(); @@ -477,7 +756,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public IList Descriptors { get; } = new List(); - public override Task BindModelAsync(ActionContext actionContext, IValueProvider valueProvider, ParameterDescriptor parameter, object value) + public override Task BindModelAsync( + ActionContext actionContext, + IModelBinder modelBinder, + IValueProvider valueProvider, + ParameterDescriptor parameter, + ModelMetadata metadata, + object value) { Descriptors.Add(parameter); @@ -596,5 +881,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public string Default { get; set; } } + + private class PageModelWithBindNeverProperty + { + [BindNever] + public string BindNeverProperty { get; set; } + } + + private class PageModelWithValidation + { + [Required] + public string Validated { get; set; } + } + + private class PageModelWithExecutors + { + public void OnGetWithBindNever([BindNever] string id) + { + } + + public void OnGet(string id) + { + } + + public void OnPostWithValidation([Required] string name) + { + } + + } + } } diff --git a/test/WebSites/RazorPagesWebSite/Pages/Validation/PageHandlerWithValidation.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageHandlerWithValidation.cshtml new file mode 100644 index 0000000000..0236c765ac --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageHandlerWithValidation.cshtml @@ -0,0 +1,10 @@ +@page +@using System.ComponentModel.DataAnnotations +@functions +{ + public void OnGet([Required(ErrorMessage = "Name is required")] string name) + { + + } +} +
diff --git a/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cs b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cs new file mode 100644 index 0000000000..607f719380 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class PageWithValidation : PageModel + { + [BindProperty(SupportsGet = true)] + [Required(ErrorMessage = "Name is required.")] + public string Name { get; set; } + + [BindProperty(SupportsGet = true)] + [Range(18, 60, ErrorMessage = "18 ≤ Age ≤ 60")] + public int Age { get; set; } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cshtml new file mode 100644 index 0000000000..845495ea59 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Validation/PageWithValidation.cshtml @@ -0,0 +1,3 @@ +@page +@model PageWithValidation +
diff --git a/test/WebSites/RazorPagesWebSite/Pages/Validation/_ViewImports.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Validation/_ViewImports.cshtml new file mode 100644 index 0000000000..aaf882de29 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Validation/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"