diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/BindPropertyAttribute.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/BindPropertyAttribute.cs new file mode 100644 index 0000000000..9146b33ec6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/BindPropertyAttribute.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class BindPropertyAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata + { + private BindingSource _bindingSource; + + public bool SupportsGet { get; set; } + + public Type BinderType { get; set; } + + /// + public virtual BindingSource BindingSource + { + get + { + if (_bindingSource == null && BinderType != null) + { + return BindingSource.Custom; + } + + return _bindingSource; + } + protected set + { + _bindingSource = value; + } + } + + /// + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs index feb6298f9b..8eecaa6772 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs @@ -30,15 +30,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } /// - /// Gets or sets the of the page. + /// Gets the list of handler methods for the page. /// - public TypeInfo PageTypeInfo { get; set; } + public IList HandlerMethods { get; set; } + + /// + /// Gets or sets the of the type that defines handler methods for the page. This can be + /// the same as and if the page does not have an + /// explicit model type defined. + /// + public TypeInfo HandlerTypeInfo { get; set; } /// /// Gets or sets the of the model. /// public TypeInfo ModelTypeInfo { get; set; } - public IList HandlerMethods { get; } = new List(); + /// + /// Gets or sets the of the page. + /// + public TypeInfo PageTypeInfo { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs index a4165252af..8291970a4a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs @@ -1,25 +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; +using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class HandlerMethodDescriptor { - public MethodInfo Method { get; set; } - - public Func> Executor { get; set; } + public MethodInfo MethodInfo { get; set; } public string HttpMethod { get; set; } - public StringSegment FormAction { get; set; } + public string FormAction { get; set; } - public HandlerParameterDescriptor[] Parameters { get; set; } - - public bool OnPage { get; set; } + public IList Parameters { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs index a11410eeb5..dcf212e077 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs @@ -1,7 +1,6 @@ // 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.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -9,8 +8,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class HandlerParameterDescriptor : ParameterDescriptor { - public object DefaultValue { get; set; } - - public ParameterInfo Parameter { get; set; } + 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 new file mode 100644 index 0000000000..37f97138f4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs @@ -0,0 +1,15 @@ +// 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.RazorPages.Infrastructure +{ + public class PageBoundPropertyDescriptor : ParameterDescriptor + { + public PropertyInfo Property { get; set; } + + public bool SupportsGet { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PagesBaseClassAttribute.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PagesBaseClassAttribute.cs new file mode 100644 index 0000000000..d16e84c72d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PagesBaseClassAttribute.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + /// + /// An attribute for base classes for Pages and PageModels. Applying this attribute to a type + /// suppresses discovery of handler methods and bound properties for that type. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PagesBaseClassAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs index 3ef2339520..e2f07618ab 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs @@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure if (ambiguousMatches != null) { - var ambiguousMethods = string.Join(", ", ambiguousMatches.Select(m => m.Method)); + var ambiguousMethods = string.Join(", ", ambiguousMatches.Select(m => m.MethodInfo)); throw new InvalidOperationException(Resources.FormatAmbiguousHandler(Environment.NewLine, ambiguousMethods)); } @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { continue; } - else if (handler.FormAction.HasValue && + else if (handler.FormAction != null && !handler.FormAction.Equals(formAction, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index 6da72a0661..c031444c98 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -1,9 +1,13 @@ // 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.Reflection; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { @@ -20,21 +24,249 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) { - var compilationResult = _compiler.Compile(actionDescriptor.RelativePath); - var compiledTypeInfo = compilationResult.CompiledType.GetTypeInfo(); - // If a model type wasn't set in code then the model property's type will be the same - // as the compiled type. - var modelTypeInfo = compiledTypeInfo.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo(); - if (modelTypeInfo == compiledTypeInfo) + var result = _compiler.Compile(actionDescriptor.RelativePath); + return CreateDescriptor(actionDescriptor, result.CompiledType.GetTypeInfo()); + } + + // Internal for unit testing + internal static CompiledPageActionDescriptor CreateDescriptor(PageActionDescriptor actionDescriptor, TypeInfo pageType) + { + // Pages always have a model type. If it's not set explicitly by the developer using + // @model, it will be the same as the page type. + // + // However, we allow it to be null here for ease of testing. + var modelType = pageType.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo(); + + // Now we want to find the handler methods. If the model defines any handlers, then we'll use those, + // otherwise look at the page itself (unless the page IS the model, in which case we already looked). + TypeInfo handlerType; + + var handlerMethods = modelType == null ? null : CreateHandlerMethods(modelType); + if (handlerMethods?.Length > 0) { - modelTypeInfo = null; + handlerType = modelType; } + else + { + handlerType = pageType; + handlerMethods = CreateHandlerMethods(pageType); + } + + var boundProperties = CreateBoundProperties(handlerType); return new CompiledPageActionDescriptor(actionDescriptor) { - PageTypeInfo = compiledTypeInfo, - ModelTypeInfo = modelTypeInfo, + ActionConstraints = actionDescriptor.ActionConstraints, + AttributeRouteInfo = actionDescriptor.AttributeRouteInfo, + BoundProperties = boundProperties, + FilterDescriptors = actionDescriptor.FilterDescriptors, + HandlerMethods = handlerMethods, + HandlerTypeInfo = handlerType, + ModelTypeInfo = modelType, + RouteValues = actionDescriptor.RouteValues, + PageTypeInfo = pageType, + Properties = actionDescriptor.Properties, }; } + + internal static HandlerMethodDescriptor[] CreateHandlerMethods(TypeInfo type) + { + var methods = type.GetMethods(); + var results = new List(); + + for (var i = 0; i < methods.Length; i++) + { + var method = methods[i]; + if (!IsValidHandlerMethod(method)) + { + continue; + } + + if (method.IsDefined(typeof(NonHandlerAttribute))) + { + continue; + } + + if (method.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute))) + { + continue; + } + + if (!TryParseHandlerMethod(method.Name, out var httpMethod, out var formAction)) + { + continue; + } + + var parameters = CreateHandlerParameters(method); + + var handlerMethodDescriptor = new HandlerMethodDescriptor() + { + MethodInfo = method, + FormAction = formAction, + HttpMethod = httpMethod, + Parameters = parameters, + }; + + results.Add(handlerMethodDescriptor); + } + + return results.ToArray(); + } + + // Internal for testing + internal static bool TryParseHandlerMethod(string methodName, out string httpMethod, out string handler) + { + httpMethod = null; + handler = null; + + // Handler method names always start with "On" + if (!methodName.StartsWith("On") || methodName.Length <= "On".Length) + { + return false; + } + + // Now we parse the method name according to our conventions to determine the required HTTP method + // and optional 'handler name'. + // + // Valid names look like: + // - OnGet + // - OnPost + // - OnFooBar + // - OnTraceAsync + // - OnPostEditAsync + + var start = "On".Length; + var length = methodName.Length; + if (methodName.EndsWith("Async", StringComparison.Ordinal)) + { + length -= "Async".Length; + } + + if (start == length) + { + // There are no additional characters. This is "On" or "OnAsync". + return false; + } + + // The http method follows "On" and is required to be at least one character. We use casing + // to determine where it ends. + var handlerNameStart = start + 1; + for (; handlerNameStart < length; handlerNameStart++) + { + if (char.IsUpper(methodName[handlerNameStart])) + { + break; + } + } + + httpMethod = methodName.Substring(start, handlerNameStart - start); + + // The handler name follows the http method and is optional. It includes everything up to the end + // excluding the "Async" suffix (if present). + handler = handlerNameStart == length ? null : methodName.Substring(handlerNameStart, length - handlerNameStart); + return true; + } + + private static bool IsValidHandlerMethod(MethodInfo methodInfo) + { + // The SpecialName bit is set to flag members that are treated in a special way by some compilers + // (such as property accessors and operator overloading methods). + if (methodInfo.IsSpecialName) + { + return false; + } + + // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. + if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + { + return false; + } + + if (methodInfo.IsStatic) + { + return false; + } + + if (methodInfo.IsAbstract) + { + return false; + } + + if (methodInfo.IsConstructor) + { + return false; + } + + if (methodInfo.IsGenericMethod) + { + return false; + } + + return methodInfo.IsPublic; + } + + // Internal for testing + internal static HandlerParameterDescriptor[] CreateHandlerParameters(MethodInfo methodInfo) + { + var methodParameters = methodInfo.GetParameters(); + var parameters = new HandlerParameterDescriptor[methodParameters.Length]; + + for (var i = 0; i < methodParameters.Length; i++) + { + var parameter = methodParameters[i]; + + parameters[i] = new HandlerParameterDescriptor() + { + BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes()), + Name = parameter.Name, + ParameterInfo = parameter, + ParameterType = parameter.ParameterType, + }; + } + + return parameters; + } + + // Internal for testing + internal static PageBoundPropertyDescriptor[] CreateBoundProperties(TypeInfo type) + { + var properties = PropertyHelper.GetVisibleProperties(type.AsType()); + + // If the type has a [BindPropertyAttribute] then we'll consider any and all public properties bindable. + var bindPropertyOnType = type.GetCustomAttribute(); + + var results = new List(); + for (var i = 0; i < properties.Length; i++) + { + var property = properties[i]; + var bindingInfo = BindingInfo.GetBindingInfo(property.Property.GetCustomAttributes()); + + if (bindingInfo == null && bindPropertyOnType == null) + { + continue; + } + + if (property.Property.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute))) + { + continue; + } + + var bindPropertyOnProperty = property.Property.GetCustomAttribute(); + var supportsGet = bindPropertyOnProperty?.SupportsGet ?? bindPropertyOnType?.SupportsGet ?? false; + + var descriptor = new PageBoundPropertyDescriptor() + { + BindingInfo = bindingInfo ?? new BindingInfo(), + Name = property.Name, + Property = property.Property, + ParameterType = property.Property.PropertyType, + SupportsGet = supportsGet, + }; + + results.Add(descriptor); + } + + return results.ToArray(); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs index 8d99d701da..3b21dd7778 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; @@ -12,38 +13,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public static class ExecutorFactory { - public static Func> CreateExecutor( - CompiledPageActionDescriptor actionDescriptor, - MethodInfo method, - HandlerParameterDescriptor[] parameters) + public static Func> CreateExecutor(HandlerMethodDescriptor handlerDescriptor) { - if (actionDescriptor == null) + if (handlerDescriptor == null) { - throw new ArgumentNullException(nameof(actionDescriptor)); + throw new ArgumentNullException(nameof(handlerDescriptor)); } + + var handler = CreateHandlerMethod(handlerDescriptor); - if (method == null) - { - throw new ArgumentNullException(nameof(method)); - } - - if (parameters == null) - { - throw new ArgumentNullException(nameof(parameters)); - } - - var methodIsDeclaredOnPage = method.DeclaringType.GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo); - var handler = CreateHandlerMethod(method, parameters); - - return async (receiver, arguments) => - { - var result = await handler.Execute(receiver, arguments); - return result; - }; + return handler.Execute; } - private static HandlerMethod CreateHandlerMethod(MethodInfo method, HandlerParameterDescriptor[] parameters) + private static HandlerMethod CreateHandlerMethod(HandlerMethodDescriptor handlerDescriptor) { + var method = handlerDescriptor.MethodInfo; + var parameters = handlerDescriptor.Parameters.ToArray(); + var methodParameters = method.GetParameters(); var returnType = method.ReturnType; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 104c53af00..d15507be68 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -327,7 +328,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } _pageContext.ViewStarts = viewStarts; - if (actionDescriptor.ModelTypeInfo == null) + if (actionDescriptor.ModelTypeInfo == actionDescriptor.PageTypeInfo) { _model = _page; } @@ -341,10 +342,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _pageContext.ViewData.Model = _model; } - if (CacheEntry.PropertyBinder != null && - !string.Equals(_pageContext.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + if (CacheEntry.PropertyBinder != null) { - // Don't bind properties on GET requests await CacheEntry.PropertyBinder(_page, _model); } @@ -379,8 +378,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var arguments = await GetArguments(handler); - var executor = handler.Executor; - result = await executor(handler.OnPage ? _page : _model, arguments); + Func> executor = null; + for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++) + { + if (object.ReferenceEquals(handler, actionDescriptor.HandlerMethods[i])) + { + executor = CacheEntry.Executors[i]; + break; + } + } + + var instance = actionDescriptor.ModelTypeInfo == actionDescriptor.HandlerTypeInfo ? _model : _page; + result = await executor(instance, arguments); } if (result == null) @@ -393,10 +402,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private async Task GetArguments(HandlerMethodDescriptor handler) { - var arguments = new object[handler.Parameters.Length]; + var arguments = new object[handler.Parameters.Count]; var valueProvider = await CompositeValueProvider.CreateAsync(_pageContext, _pageContext.ValueProviderFactories); - for (var i = 0; i < handler.Parameters.Length; i++) + for (var i = 0; i < handler.Parameters.Count; i++) { var parameter = handler.Parameters[i]; @@ -406,7 +415,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal parameter, value: null); - arguments[i] = result.IsModelSet ? result.Model : parameter.DefaultValue; + if (result.IsModelSet) + { + arguments[i] = result.Model; + } + else if (parameter.ParameterInfo.HasDefaultValue) + { + arguments[i] = parameter.ParameterInfo.DefaultValue; + } + else if (parameter.ParameterType.GetTypeInfo().IsValueType) + { + arguments[i] = Activator.CreateInstance(parameter.ParameterType); + } } return arguments; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs index 6e148ce7b1..0894825fad 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -19,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Func modelFactory, Action releaseModel, Func propertyBinder, + Func>[] executors, IReadOnlyList> viewStartFactories, FilterItem[] cacheableFilters) { @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ModelFactory = modelFactory; ReleaseModel = releaseModel; PropertyBinder = propertyBinder; + Executors = executors; ViewStartFactories = viewStartFactories; CacheableFilters = cacheableFilters; } @@ -54,6 +55,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal /// public Func PropertyBinder { get; } + public Func>[] Executors { get; } + /// /// Gets the applicable ViewStart pages. /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 6b4e12e057..a11933d492 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -181,20 +182,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Func modelFactory = null; Action modelReleaser = null; - if (compiledActionDescriptor.ModelTypeInfo == null) + if (compiledActionDescriptor.ModelTypeInfo != compiledActionDescriptor.PageTypeInfo) { - PopulateHandlerMethodDescriptors(compiledActionDescriptor.PageTypeInfo, compiledActionDescriptor); - } - else - { - PopulateHandlerMethodDescriptors(compiledActionDescriptor.ModelTypeInfo, compiledActionDescriptor); - modelFactory = _modelFactoryProvider.CreateModelFactory(compiledActionDescriptor); modelReleaser = _modelFactoryProvider.CreateModelDisposer(compiledActionDescriptor); } var viewStartFactories = GetViewStartFactories(compiledActionDescriptor); + var executors = GetExecutors(compiledActionDescriptor); + return new PageActionInvokerCacheEntry( compiledActionDescriptor, pageFactory, @@ -202,6 +199,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal modelFactory, modelReleaser, propertyBinder, + executors, viewStartFactories, cachedFilters); } @@ -226,82 +224,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return viewStartFactories; } - // Internal for testing. - internal static void PopulateHandlerMethodDescriptors(TypeInfo type, CompiledPageActionDescriptor actionDescriptor) - { - var methods = type.GetMethods(); - for (var i = 0; i < methods.Length; i++) - { - var method = methods[i]; - if (!IsValidHandler(method)) - { - continue; - } - - string httpMethod; - int formActionStart; - - if (method.Name.StartsWith("OnGet", StringComparison.Ordinal)) - { - httpMethod = "GET"; - formActionStart = "OnGet".Length; - } - else if (method.Name.StartsWith("OnPost", StringComparison.Ordinal)) - { - httpMethod = "POST"; - formActionStart = "OnPost".Length; - } - else - { - continue; - } - - var formActionLength = method.Name.Length - formActionStart; - if (method.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase)) - { - formActionLength -= "Async".Length; - } - - var formAction = new StringSegment(method.Name, formActionStart, formActionLength); - - var parameters = GetHandlerParameters(method); - - var handlerMethodDescriptor = new HandlerMethodDescriptor - { - Method = method, - Executor = ExecutorFactory.CreateExecutor(actionDescriptor, method, parameters), - FormAction = formAction, - HttpMethod = httpMethod, - Parameters = parameters, - OnPage = actionDescriptor.PageTypeInfo == type, - }; - - actionDescriptor.HandlerMethods.Add(handlerMethodDescriptor); - } - } - - private static HandlerParameterDescriptor[] GetHandlerParameters(MethodInfo methodInfo) - { - var methodParameters = methodInfo.GetParameters(); - var parameters = new HandlerParameterDescriptor[methodParameters.Length]; - - for (var i = 0; i < methodParameters.Length; i++) - { - var parameter = methodParameters[i]; - - parameters[i] = new HandlerParameterDescriptor() - { - BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes()), - DefaultValue = GetDefaultValue(parameter), - Name = parameter.Name, - Parameter = parameter, - ParameterType = parameter.ParameterType, - }; - } - - return parameters; - } - private static object GetDefaultValue(ParameterInfo methodParameter) { object defaultValue = null; @@ -317,42 +239,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return defaultValue; } - private static bool IsValidHandler(MethodInfo methodInfo) + private static Func>[] GetExecutors(CompiledPageActionDescriptor actionDescriptor) { - // The SpecialName bit is set to flag members that are treated in a special way by some compilers - // (such as property accessors and operator overloading methods). - if (methodInfo.IsSpecialName) + if (actionDescriptor.HandlerMethods == null || actionDescriptor.HandlerMethods.Count == 0) { - return false; + return Array.Empty>>(); } - // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. - if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + var results = new Func>[actionDescriptor.HandlerMethods.Count]; + + for (var i = 0; i < actionDescriptor.HandlerMethods.Count; i++) { - return false; + results[i] = ExecutorFactory.CreateExecutor(actionDescriptor.HandlerMethods[i]); } - if (methodInfo.IsStatic) - { - return false; - } - - if (methodInfo.IsAbstract) - { - return false; - } - - if (methodInfo.IsConstructor) - { - return false; - } - - if (methodInfo.IsGenericMethod) - { - return false; - } - - return methodInfo.IsPublic; + return results; } internal class InnerCache diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs index ab8f551848..15ec5e1684 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PagePropertyBinderFactory.cs @@ -3,12 +3,11 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Internal; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { @@ -29,15 +28,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal throw new ArgumentNullException(nameof(actionDescriptor)); } - var bindPropertiesOnPage = actionDescriptor.ModelTypeInfo == null; - var target = bindPropertiesOnPage ? actionDescriptor.PageTypeInfo : actionDescriptor.ModelTypeInfo; - var propertiesToBind = GetPropertiesToBind(modelMetadataProvider, target); - - if (propertiesToBind.Count == 0) + var properties = actionDescriptor.BoundProperties; + if (properties == null || properties.Count == 0) { return null; } + var isHandlerThePage = actionDescriptor.HandlerTypeInfo == actionDescriptor.PageTypeInfo; + + 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(Page page, object model) @@ -47,14 +52,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal throw new ArgumentNullException(nameof(page)); } - if (!bindPropertiesOnPage && model == null) + if (!isHandlerThePage && model == null) { throw new ArgumentNullException(nameof(model)); } var pageContext = page.PageContext; - var instance = bindPropertiesOnPage ? page : model; - return BindPropertiesAsync(parameterBinder, pageContext, instance, propertiesToBind); + var instance = isHandlerThePage ? page : model; + return BindPropertiesAsync(parameterBinder, pageContext, instance, properties, metadata); } } @@ -62,92 +67,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ParameterBinder parameterBinder, PageContext pageContext, object instance, - IList propertiesToBind) + IList properties, + IList metadata) { - var valueProvider = await GetCompositeValueProvider(pageContext); - for (var i = 0; i < propertiesToBind.Count; i++) + var isGet = string.Equals("GET", pageContext.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase); + + var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); + for (var i = 0; i < properties.Count; i++) { - var propertyBindingInfo = propertiesToBind[i]; - var modelBindingResult = await parameterBinder.BindModelAsync( - pageContext, - valueProvider, - propertyBindingInfo.ParameterDescriptor); - if (modelBindingResult.IsModelSet) - { - var modelMetadata = propertyBindingInfo.ModelMetadata; - PropertyValueSetter.SetValue( - modelMetadata, - instance, - modelBindingResult.Model); - } - } - } - - private static IList GetPropertiesToBind( - IModelMetadataProvider modelMetadataProvider, - TypeInfo handlerSourceTypeInfo) - { - var handlerType = handlerSourceTypeInfo.AsType(); - var properties = PropertyHelper.GetVisibleProperties(type: handlerType); - var typeMetadata = modelMetadataProvider.GetMetadataForType(handlerType); - - var propertyBindingInfo = new List(); - for (var i = 0; i < properties.Length; i++) - { - var property = properties[i]; - var bindingInfo = BindingInfo.GetBindingInfo(property.Property.GetCustomAttributes()); - - if (bindingInfo == null) + if (isGet && !((PageBoundPropertyDescriptor)properties[i]).SupportsGet) { continue; } - var propertyMetadata = typeMetadata.Properties[property.Name] ?? - modelMetadataProvider.GetMetadataForProperty(handlerType, property.Name); - if (propertyMetadata == null) + var result = await parameterBinder.BindModelAsync(pageContext, valueProvider, properties[i]); + if (result.IsModelSet) { - continue; + PropertyValueSetter.SetValue(metadata[i], instance, result.Model); } - - var parameterDescriptor = new ParameterDescriptor - { - BindingInfo = bindingInfo, - Name = property.Name, - ParameterType = property.Property.PropertyType, - }; - - propertyBindingInfo.Add(new PropertyBindingInfo(parameterDescriptor, propertyMetadata)); } - - return propertyBindingInfo; - } - - private static async Task GetCompositeValueProvider(PageContext pageContext) - { - var factories = pageContext.ValueProviderFactories; - var valueProviderFactoryContext = new ValueProviderFactoryContext(pageContext); - for (var i = 0; i < factories.Count; i++) - { - var factory = factories[i]; - await factory.CreateValueProviderAsync(valueProviderFactoryContext); - } - - return new CompositeValueProvider(valueProviderFactoryContext.ValueProviders); - } - - private struct PropertyBindingInfo - { - public PropertyBindingInfo( - ParameterDescriptor parameterDescriptor, - ModelMetadata modelMetadata) - { - ParameterDescriptor = parameterDescriptor; - ModelMetadata = modelMetadata; - } - - public ParameterDescriptor ParameterDescriptor { get; } - - public ModelMetadata ModelMetadata { get; } } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/NonHandlerAttribute.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/NonHandlerAttribute.cs new file mode 100644 index 0000000000..5c9cf53994 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/NonHandlerAttribute.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// Specifies that the targeted method is not a page handler method. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class NonHandlerAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs index a906de2141..f90b3365d4 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// /// A base class for a Razor page. /// + [PagesBaseClass] public abstract class Page : RazorPageBase, IRazorPage { private IObjectModelValidator _objectValidator; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs index 0438afc757..54ec258e24 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -22,6 +22,7 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.RazorPages { + [PagesBaseClass] public abstract class PageModel { private IObjectModelValidator _objectValidator; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index bd705a348a..7624f5335a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -716,6 +716,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.DoesNotContain(validationError, content); } + [Fact] + public async Task PageProperty_WithSupportsGet_BoundInGet() + { + // Arrange + var expected = "

11

"; + var request = new HttpRequestMessage(HttpMethod.Get, "Pages/PropertyBinding/BindPropertyWithGet?value=11"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.StartsWith(expected, content.Trim()); + } + [Fact] public async Task PagePropertiesAreInjected() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs index d5536d1209..a940e3b1bf 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs @@ -2,6 +2,8 @@ // 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.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Routing; @@ -30,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -67,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor, }, @@ -109,7 +111,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -140,20 +142,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Delete"), + FormAction = "Delete", }; var pageContext = new PageContext { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -190,20 +192,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Delete"), + FormAction = "Delete", }; var pageContext = new PageContext { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -240,20 +242,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Delete"), + FormAction = "Delete", }; var pageContext = new PageContext { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -285,20 +287,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Delete"), + FormAction = "Delete", }; var pageContext = new PageContext { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -336,20 +338,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Delete"), + FormAction = "Delete", }; var pageContext = new PageContext { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -381,7 +383,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor1 = new HandlerMethodDescriptor { HttpMethod = "POST", - FormAction = new StringSegment("Subscribe"), + FormAction = "Subscribe", }; var descriptor2 = new HandlerMethodDescriptor @@ -393,7 +395,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -429,13 +431,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Arrange var descriptor1 = new HandlerMethodDescriptor { - Method = GetType().GetMethod(nameof(Post)), + MethodInfo = GetType().GetMethod(nameof(Post)), HttpMethod = "POST", }; var descriptor2 = new HandlerMethodDescriptor { - Method = GetType().GetMethod(nameof(PostAsync)), + MethodInfo = GetType().GetMethod(nameof(PostAsync)), HttpMethod = "POST", }; @@ -448,7 +450,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -468,7 +470,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Act & Assert var ex = Assert.Throws(() => selector.Select(pageContext)); - var methods = descriptor1.Method + ", " + descriptor2.Method; + var methods = descriptor1.MethodInfo + ", " + descriptor2.MethodInfo; var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" + Environment.NewLine + Environment.NewLine + methods; @@ -481,16 +483,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Arrange var descriptor1 = new HandlerMethodDescriptor { - Method = GetType().GetMethod(nameof(Post)), + MethodInfo = GetType().GetMethod(nameof(Post)), HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor2 = new HandlerMethodDescriptor { - Method = GetType().GetMethod(nameof(PostAsync)), + MethodInfo = GetType().GetMethod(nameof(PostAsync)), HttpMethod = "POST", - FormAction = new StringSegment("Add"), + FormAction = "Add", }; var descriptor3 = new HandlerMethodDescriptor @@ -502,7 +504,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ActionDescriptor = new CompiledPageActionDescriptor { - HandlerMethods = + HandlerMethods = new List() { descriptor1, descriptor2, @@ -528,7 +530,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Act & Assert var ex = Assert.Throws(() => selector.Select(pageContext)); - var methods = descriptor1.Method + ", " + descriptor2.Method; + var methods = descriptor1.MethodInfo + ", " + descriptor2.MethodInfo; var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" + Environment.NewLine + Environment.NewLine + methods; diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs new file mode 100644 index 0000000000..4b27eafd17 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs @@ -0,0 +1,676 @@ +// 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.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageLoaderTest + { + [Fact] + public void CreateDescriptor_CopiesPropertiesFromBaseClass() + { + // Arrange + var expected = new PageActionDescriptor() // We only copy the properties that are meaningful for pages. + { + ActionConstraints = new List(), + AttributeRouteInfo = new AttributeRouteInfo(), + FilterDescriptors = new List(), + RelativePath = "/Foo", + RouteValues = new Dictionary(), + ViewEnginePath = "/Pages/Foo", + }; + + // Act + var actual = DefaultPageLoader.CreateDescriptor(expected, typeof(EmptyPage).GetTypeInfo()); + + // Assert + Assert.Same(expected.ActionConstraints, actual.ActionConstraints); + Assert.Same(expected.AttributeRouteInfo, actual.AttributeRouteInfo); + Assert.Same(expected.FilterDescriptors, actual.FilterDescriptors); + Assert.Same(expected.Properties, actual.Properties); + Assert.Same(expected.RelativePath, actual.RelativePath); + Assert.Same(expected.RouteValues, actual.RouteValues); + Assert.Same(expected.ViewEnginePath, actual.ViewEnginePath); + } + + // We want to test the the 'empty' page has no bound properties, and no handler methods. + [Fact] + public void CreateDescriptor_EmptyPage() + { + // Arrange + var type = typeof(EmptyPage).GetTypeInfo(); + + // Act + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + + // Assert + Assert.Empty(result.BoundProperties); + Assert.Empty(result.HandlerMethods); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.HandlerTypeInfo); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.ModelTypeInfo); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.PageTypeInfo); + } + + // We want to test the the 'empty' page and pagemodel has no bound properties, and no handler methods. + [Fact] + public void CreateDescriptor_EmptyPageModel() + { + // Arrange + var type = typeof(EmptyPageWithPageModel).GetTypeInfo(); + + // Act + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + + // Assert + Assert.Empty(result.BoundProperties); + Assert.Empty(result.HandlerMethods); + Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.HandlerTypeInfo); + Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), result.ModelTypeInfo); + Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.PageTypeInfo); + } + + private class EmptyPage : Page + { + // Copied from generated code + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; + public EmptyPage Model => ViewData.Model; + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class EmptyPageWithPageModel : Page + { + // Copied from generated code + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; + public EmptyPageModel Model => ViewData.Model; + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class EmptyPageModel : PageModel + { + } + + [Fact] // If the model has handler methods, we prefer those. + public void CreateDescriptor_FindsHandlerMethod_OnModel() + { + // Arrange + var type = typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(); + + // Act + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + + // Assert + Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); + Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); + Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.HandlerTypeInfo); + Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.ModelTypeInfo); + Assert.Same(typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(), result.PageTypeInfo); + } + + private class ModelWithHandler + { + [ModelBinder] + public int BindMe { get; set; } + + public void OnGet() { } + } + + private class PageWithHandlerThatGetsIgnored + { + public ModelWithHandler Model => null; + + [ModelBinder] + public int IgnoreMe { get; set; } + + public void OnPost() { } + } + + + [Fact] // If the model has no handler methods, we look at the page instead. + public void CreateDescriptor_FindsHandlerMethodOnPage_WhenModelHasNoHandlers() + { + // Arrange + var type = typeof(PageWithHandler).GetTypeInfo(); + + // Act + var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), type); + + // Assert + Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); + Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); + Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.HandlerTypeInfo); + Assert.Same(typeof(PocoModel).GetTypeInfo(), result.ModelTypeInfo); + Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.PageTypeInfo); + } + + private class PocoModel + { + // Just a plain ol' model, nothing to see here. + + [ModelBinder] + public int IgnoreMe { get; set; } + } + + private class PageWithHandler + { + public PocoModel Model => null; + + [ModelBinder] + public int BindMe { get; set; } + + public void OnGet() { } + } + + [Fact] + public void CreateHandlerMethods_DiscoversHandlersFromBaseType() + { + // Arrange + var type = typeof(InheritsMethods).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Collection( + results.OrderBy(h => h.MethodInfo.Name).ToArray(), + (handler) => + { + Assert.Equal("OnGet", handler.MethodInfo.Name); + Assert.Equal(typeof(InheritsMethods), handler.MethodInfo.DeclaringType); + }, + (handler) => + { + Assert.Equal("OnGet", handler.MethodInfo.Name); + Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); + }, + (handler) => + { + Assert.Equal("OnPost", handler.MethodInfo.Name); + Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); + }); + } + + private class TestSetPageModel + { + public void OnGet() + { + } + + public void OnPost() + { + } + } + + private class TestSetPageWithModel + { + public TestSetPageModel Model { get; set; } + } + + private class InheritsMethods : TestSetPageModel + { + public new void OnGet() + { + } + } + + [Fact] + public void CreateHandlerMethods_IgnoresNonPublicMethods() + { + // Arrange + var type = typeof(ProtectedModel).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Empty(results); + } + + private class ProtectedModel + { + protected void OnGet() + { + } + + private void OnPost() + { + } + } + + [Fact] + public void CreateHandlerMethods_IgnoreGenericTypeParameters() + { + // Arrange + var type = typeof(GenericClassModel).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Empty(results); + } + + private class GenericClassModel + { + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerMethods_IgnoresStaticMethods() + { + // Arrange + var type = typeof(PageModelWithStaticHandler).GetTypeInfo(); + var expected = type.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Collection( + results, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private class PageModelWithStaticHandler + { + public static void OnGet(string name) + { + } + + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerMethods_IgnoresAbstractMethods() + { + // Arrange + var type = typeof(PageModelWithAbstractMethod).GetTypeInfo(); + var expected = type.GetMethod(nameof(PageModelWithAbstractMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Collection( + results, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private abstract class PageModelWithAbstractMethod + { + public abstract void OnPost(string name); + + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerMethods_IgnoresMethodWithNonHandlerAttribute() + { + // Arrange + var type = typeof(PageWithNonHandlerMethod).GetTypeInfo(); + var expected = type.GetMethod(nameof(PageWithNonHandlerMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Collection( + results, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private class PageWithNonHandlerMethod + { + [NonHandler] + public void OnPost(string name) { } + + public void OnGet() + { + } + } + + // There are more tests for the parsing elsewhere, this is just testing that it's wired + // up to the descriptor. + [Fact] + public void CreateHandlerMethods_ParsesMethod() + { + // Arrange + var type = typeof(PageModelWithFormActions).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + Assert.Collection( + results.OrderBy(h => h.MethodInfo.Name), + handler => + { + Assert.Same(type.GetMethod(nameof(PageModelWithFormActions.OnPutDeleteAsync)), handler.MethodInfo); + Assert.Equal("Put", handler.HttpMethod); + Assert.Equal("Delete", handler.FormAction.ToString()); + }); + } + + private class PageModelWithFormActions + { + public void OnPutDeleteAsync() + { + } + + public void Foo() // This isn't a valid handler name. + { + } + } + + [Fact] + public void CreateHandlerMethods_AddsParameterDescriptors() + { + // Arrange + var type = typeof(PageWithHandlerParameters).GetTypeInfo(); + var expected = type.GetMethod(nameof(PageWithHandlerParameters.OnPost), BindingFlags.Public | BindingFlags.Instance); + + // Act + var results = DefaultPageLoader.CreateHandlerMethods(type); + + // Assert + var handler = Assert.Single(results); + + Assert.Collection( + handler.Parameters, + p => + { + Assert.Equal(typeof(string), p.ParameterType); + Assert.NotNull(p.ParameterInfo); + Assert.Equal("name", p.Name); + }, + p => + { + Assert.Equal(typeof(int), p.ParameterType); + Assert.NotNull(p.ParameterInfo); + Assert.Equal("id", p.Name); + Assert.Equal("personId", p.BindingInfo.BinderModelName); + }); + } + + private class PageWithHandlerParameters + { + public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { } + } + + // We're using PropertyHelper from Common to find the properties here, which implements + // out standard set of semantics for properties that the framework interacts with. + // + // One of the desirable consequences of that is we only find 'visible' properties. We're not + // retesting all of the details of PropertyHelper here, just the visibility part as a quick check + // that we're using PropertyHelper as expected. + [Fact] + public void CreateBoundProperties_UsesPropertyHelpers_ToFindProperties() + { + // Arrange + var type = typeof(HidesAProperty).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal(typeof(HidesAProperty).GetTypeInfo(), p.Property.DeclaringType.GetTypeInfo()); + }); + } + + private class HasAHiddenProperty + { + [BindProperty] + public int Property { get; set; } + } + + private class HidesAProperty : HasAHiddenProperty + { + [BindProperty] + public new int Property { get; set; } + } + + // We're using BindingInfo to make property binding opt-in here. We're not going to retest + // all of the semantics of BindingInfo here, as that's covered elsewhere. + [Fact] + public void CreateBoundProperties_UsesBindingInfo_ToFindProperties() + { + // Arrange + var type = typeof(ModelWithBindingInfoProperty).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + }); + } + + private class ModelWithBindingInfoProperty + { + [ModelBinder] + public int Property { get; set; } + + public int IgnoreMe { get; set; } + } + + // Additionally [BindProperty] on a property can opt-in a property + [Fact] + public void CreateBoundProperties_UsesBindPropertyAttribute_ToFindProperties() + { + // Arrange + var type = typeof(ModelWithBindProperty).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + }); + } + + private class ModelWithBindProperty + { + [BindProperty] + public int Property { get; set; } + + public int IgnoreMe { get; set; } + } + + // Additionally [BindProperty] on a property can opt-in a property + [Fact] + public void CreateBoundProperties_BindPropertyAttributeOnModel_OptsInAllProperties() + { + // Arrange + var type = typeof(ModelWithBindPropertyOnClass).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + }); + } + + [BindProperty] + private class ModelWithBindPropertyOnClass : EmptyPageModel + { + public int Property { get; set; } + } + + [Fact] + public void CreateBoundProperties_SupportsGet_OnProperty() + { + // Arrange + var type = typeof(ModelSupportsGetOnProperty).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + Assert.True(p.SupportsGet); + }); + } + + private class ModelSupportsGetOnProperty + { + [BindProperty(SupportsGet = true)] + public int Property { get; set; } + + public int IgnoreMe { get; set; } + } + + [Fact] + public void CreateBoundProperties_SupportsGet_OnClass() + { + // Arrange + var type = typeof(ModelSupportsGetOnClass).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + Assert.True(p.SupportsGet); + }); + } + + [BindProperty(SupportsGet = true)] + private class ModelSupportsGetOnClass : EmptyPageModel + { + public int Property { get; set; } + } + + [Fact] + public void CreateBoundProperties_SupportsGet_Override() + { + // Arrange + var type = typeof(ModelSupportsGetOverride).GetTypeInfo(); + + // Act + var results = DefaultPageLoader.CreateBoundProperties(type); + + // Assert + Assert.Collection( + results.OrderBy(p => p.Property.Name), + p => + { + Assert.Equal("Property", p.Property.Name); + Assert.False(p.SupportsGet); + }); + } + + [BindProperty(SupportsGet = true)] + private class ModelSupportsGetOverride : EmptyPageModel + { + [BindProperty(SupportsGet = false)] + public int Property { get; set; } + } + + [Theory] + [InlineData("Foo")] + [InlineData("On")] + [InlineData("OnAsync")] + [InlineData("Async")] + public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName) + { + // Arrange + + // Act + var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + + // Assert + Assert.False(result); + Assert.Null(httpMethod); + Assert.Null(handler); + } + + [Theory] + [InlineData("OnG", "G", null)] + [InlineData("OnGAsync", "G", null)] + [InlineData("OnPOST", "P", "OST")] + [InlineData("OnPOSTAsync", "P", "OST")] + [InlineData("OnDeleteFoo", "Delete", "Foo")] + [InlineData("OnDeleteFooAsync", "Delete", "Foo")] + [InlineData("OnMadeupLongHandlerName", "Madeup", "LongHandlerName")] + [InlineData("OnMadeupLongHandlerNameAsync", "Madeup", "LongHandlerName")] + public void TryParseHandler_ParsesHandlerNames_ValidData(string methodName, string expectedHttpMethod, string expectedHandler) + { + // Arrange + + // Act + var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + + // Assert + Assert.True(result); + Assert.Equal(expectedHttpMethod, httpMethod); + Assert.Equal(expectedHandler, handler); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs index fdd54c378b..f0339d2018 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs @@ -16,17 +16,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal public class ExecutorFactoryTest { [Fact] - public async Task CreateExecutor_ForActionResultMethod_OnPage() + public async Task CreateExecutor_ForActionResultMethod() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor + var handler = new HandlerMethodDescriptor() { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), + MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturningHandler)), + Parameters = new HandlerParameterDescriptor[0], }; - var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturningHandler)); // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -36,17 +36,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult_OnPage() + public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor + var handler = new HandlerMethodDescriptor() { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), + MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ConcreteActionResult)), + Parameters = new HandlerParameterDescriptor[0], }; - var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ConcreteActionResult)); // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -56,18 +56,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters_OnPage() + public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - }; var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturnHandlerWithParameters)); - var parameters = CreateParameters(methodInfo); + var handler = new HandlerMethodDescriptor() + { + MethodInfo = methodInfo, + Parameters = CreateParameters(methodInfo), + }; // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, parameters); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -78,18 +78,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForVoidReturningMethod_OnPage() + public async Task CreateExecutor_ForVoidReturningMethod() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor + var handler = new HandlerMethodDescriptor() { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), + MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidReturningHandler)), + Parameters = new HandlerParameterDescriptor[0], }; + var page = new TestPage(); - var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidReturningHandler)); // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -100,18 +101,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForVoidTaskReturningMethod_OnPage() + public async Task CreateExecutor_ForVoidTaskReturningMethod() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor + var handler = new HandlerMethodDescriptor() { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), + MethodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidTaskReturningHandler)), + Parameters = new HandlerParameterDescriptor[0], }; + var page = new TestPage(); - var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidTaskReturningHandler)); // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -122,17 +124,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod_OnPage() + public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - }; var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.GenericTaskHandler)); + var handler = new HandlerMethodDescriptor() + { + MethodInfo = methodInfo, + Parameters = CreateParameters(methodInfo), + }; // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -142,17 +145,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal } [Fact] - public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod_OnPage() + public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod() { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - }; var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.TaskReturningConcreteSubtype)); + var handler = new HandlerMethodDescriptor() + { + MethodInfo = methodInfo, + Parameters = CreateParameters(methodInfo), + }; // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); + var executor = ExecutorFactory.CreateExecutor(handler); // Assert Assert.NotNull(executor); @@ -162,159 +166,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal Assert.Equal("value", contentResult.Content); } - [Fact] - public async Task CreateExecutor_ForActionResultMethod_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ActionResultReturningHandler)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(new TestPageModel(), null); - var actionResult = await actionResultTask; - Assert.IsType(actionResult); - } - - [Fact] - public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ConcreteActionResult)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(new TestPageModel(), null); - var actionResult = await actionResultTask; - Assert.IsType(actionResult); - } - - [Fact] - public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ActionResultReturnHandlerWithParameters)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(new TestPageModel(), CreateArguments(methodInfo)); - var actionResult = await actionResultTask; - var contentResult = Assert.IsType(actionResult); - Assert.Equal("Hello 0", contentResult.Content); - } - - [Fact] - public async Task CreateExecutor_ForVoidReturningMethod_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var model = new TestPageModel(); - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.VoidReturningHandler)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(model, null); - var actionResult = await actionResultTask; - Assert.Null(actionResult); - Assert.True(model.SideEffects); - } - - [Fact] - public async Task CreateExecutor_ForVoidTaskReturningMethod_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var model = new TestPageModel(); - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.VoidTaskReturningHandler)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0]); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(model, null); - var actionResult = await actionResultTask; - Assert.Null(actionResult); - Assert.True(model.SideEffects); - } - - [Fact] - public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.GenericTaskHandler)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(new TestPageModel(), null); - var actionResult = await actionResultTask; - Assert.IsType(actionResult); - } - - [Fact] - public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod_OnPageModel() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; - var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.TaskReturningConcreteSubtype)); - - // Act - var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, CreateParameters(methodInfo)); - - // Assert - Assert.NotNull(executor); - var actionResultTask = executor(new TestPageModel(), CreateArguments(methodInfo)); - var actionResult = await actionResultTask; - var contentResult = Assert.IsType(actionResult); - Assert.Equal("value", contentResult.Content); - } - [Theory] [InlineData(nameof(TestPageModel.StringResult))] [InlineData(nameof(TestPageModel.TaskOfObject))] @@ -322,16 +173,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal public void CreateExecutor_ThrowsIfTypeIsNotAValidReturnType(string methodName) { // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(PageModel).GetTypeInfo(), - }; var methodInfo = typeof(TestPageModel).GetMethod(methodName); + var handler = new HandlerMethodDescriptor() + { + MethodInfo = methodInfo, + Parameters = CreateParameters(methodInfo), + }; // Act & Assert - var ex = Assert.Throws(() => - ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo, new HandlerParameterDescriptor[0])); + var ex = Assert.Throws(() => ExecutorFactory.CreateExecutor(handler)); Assert.Equal($"Unsupported handler method return type '{methodInfo.ReturnType}'.", ex.Message); } @@ -365,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal { BindingInfo = BindingInfo.GetBindingInfo(p.GetCustomAttributes()), Name = p.Name, - Parameter = p, + ParameterInfo = p, ParameterType = p.ParameterType, }).ToArray(); } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 34a728e223..cd38a42cb9 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -36,27 +36,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal RelativePath = "Path1", FilterDescriptors = new FilterDescriptor[0], }; + Func factory = _ => null; Action releaser = (_, __) => { }; var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor)); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var pageFactoryProvider = new Mock(); - pageFactoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) + pageFactoryProvider + .Setup(f => f.CreatePageFactory(It.IsAny())) .Returns(factory); - pageFactoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) + pageFactoryProvider + .Setup(f => f.CreatePageDisposer(It.IsAny())) .Returns(releaser); var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), pageFactoryProvider.Object); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + var context = new ActionInvokerProviderContext(new ActionContext() + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); // Act invokerProvider.OnProvidersExecuting(context); @@ -65,106 +72,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.NotNull(context.Result); var actionInvoker = Assert.IsType(context.Result); var entry = actionInvoker.CacheEntry; - var compiledPageActionDescriptor = Assert.IsType(entry.ActionDescriptor); - Assert.Equal(descriptor.RelativePath, compiledPageActionDescriptor.RelativePath); + Assert.Equal(descriptor.RelativePath, entry.ActionDescriptor.RelativePath); Assert.Same(factory, entry.PageFactory); Assert.Same(releaser, entry.ReleasePage); Assert.Null(entry.ModelFactory); Assert.Null(entry.ReleaseModel); } - [Fact] - public void OnProvidersExecuting_CachesModelBinderFactory() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - FilterDescriptors = new FilterDescriptor[0], - }; - - var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) - .Returns(new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(PageWithBoundProperties).GetTypeInfo(), - }); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); - var pageFactoryProvider = Mock.Of(); - - var invokerProvider = CreateInvokerProvider( - loader.Object, - actionDescriptorProvider.Object, - pageFactoryProvider); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); - - // Act - invokerProvider.OnProvidersExecuting(context); - - // Assert - Assert.NotNull(context.Result); - var actionInvoker = Assert.IsType(context.Result); - var entry = actionInvoker.CacheEntry; - Assert.NotNull(entry.PropertyBinder); - } - - [Fact] - public void OnProvidersExecuting_SetsHandlers() - { - // Arrange - var descriptor = new PageActionDescriptor - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - }; - Func factory = _ => null; - Action releaser = (_, __) => { }; - - var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestSetPageWithModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); - var pageFactoryProvider = new Mock(); - pageFactoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) - .Returns(factory); - pageFactoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) - .Returns(releaser); - - var modelFactoryProvider = new Mock(); - - var invokerProvider = CreateInvokerProvider( - loader.Object, - actionDescriptorProvider.Object, - pageFactoryProvider.Object, - modelFactoryProvider.Object); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); - - // Act - invokerProvider.OnProvidersExecuting(context); - - // Assert - Assert.NotNull(context.Result); - var actionInvoker = Assert.IsType(context.Result); - var entry = actionInvoker.CacheEntry; - - Assert.Collection(entry.ActionDescriptor.HandlerMethods, - handlerDescriptor => - { - Assert.Equal(nameof(TestSetPageModel.OnGet), handlerDescriptor.Method.Name); - Assert.NotNull(handlerDescriptor.Executor); - }, - handlerDescriptor => - { - Assert.Equal(nameof(TestSetPageModel.OnPost), handlerDescriptor.Method.Name); - Assert.NotNull(handlerDescriptor.Executor); - }); - } - [Fact] public void OnProvidersExecuting_WithModel_PopulatesCacheEntry() { @@ -174,36 +88,45 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal RelativePath = "Path1", FilterDescriptors = new FilterDescriptor[0], }; + Func factory = _ => null; Action releaser = (_, __) => { }; Func modelFactory = _ => null; Action modelDisposer = (_, __) => { }; var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var pageFactoryProvider = new Mock(); - pageFactoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) + pageFactoryProvider + .Setup(f => f.CreatePageFactory(It.IsAny())) .Returns(factory); - pageFactoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) + pageFactoryProvider + .Setup(f => f.CreatePageDisposer(It.IsAny())) .Returns(releaser); var modelFactoryProvider = new Mock(); - modelFactoryProvider.Setup(f => f.CreateModelFactory(It.IsAny())) + modelFactoryProvider + .Setup(f => f.CreateModelFactory(It.IsAny())) .Returns(modelFactory); - modelFactoryProvider.Setup(f => f.CreateModelDisposer(It.IsAny())) + modelFactoryProvider + .Setup(f => f.CreateModelDisposer(It.IsAny())) .Returns(modelDisposer); var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), pageFactoryProvider.Object, modelFactoryProvider.Object); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + var context = new ActionInvokerProviderContext(new ActionContext() + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); // Act invokerProvider.OnProvidersExecuting(context); @@ -232,18 +155,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }; var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var razorPageFactoryProvider = new Mock(); + Func factory1 = () => null; Func factory2 = () => null; + razorPageFactoryProvider .Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(factory1, new IChangeToken[0])); - razorPageFactoryProvider.Setup(f => f.CreateFactory("/_ViewStart.cshtml")) + razorPageFactoryProvider + .Setup(f => f.CreateFactory("/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(factory2, new[] { Mock.Of() })); var fileProvider = new TestFileProvider(); @@ -254,11 +179,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), razorPageFactoryProvider: razorPageFactoryProvider.Object, razorProject: defaultRazorProject); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + var context = new ActionInvokerProviderContext(new ActionContext() + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); // Act invokerProvider.OnProvidersExecuting(context); @@ -270,49 +200,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories); } - [Fact] - public void OnProvidersExecuting_CachesExecutor() - { - // Arrange - var descriptor = new PageActionDescriptor - { - RelativePath = "/Home/Path1/File.cshtml", - ViewEnginePath = "/Home/Path1/File.cshtml", - FilterDescriptors = new FilterDescriptor[0], - }; - - var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); - var razorPageFactoryProvider = new Mock(); - var fileProvider = new TestFileProvider(); - var defaultRazorProject = new TestRazorProject(fileProvider); - - var invokerProvider = CreateInvokerProvider( - loader.Object, - actionDescriptorProvider.Object, - razorPageFactoryProvider: razorPageFactoryProvider.Object, - razorProject: defaultRazorProject); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); - - // Act - invokerProvider.OnProvidersExecuting(context); - - // Assert - Assert.NotNull(context.Result); - var actionInvoker = Assert.IsType(context.Result); - var actionDescriptor = actionInvoker.CacheEntry.ActionDescriptor; - Assert.Collection(actionDescriptor.HandlerMethods, - handlerDescriptor => - { - Assert.Equal(nameof(TestPageModel.OnGet), handlerDescriptor.Method.Name); - Assert.NotNull(handlerDescriptor.Executor); - }); - } [Fact] public void OnProvidersExecuting_CachesEntries() @@ -323,18 +210,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal RelativePath = "Path1", FilterDescriptors = new FilterDescriptor[0], }; + var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor)); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + CreateActionDescriptorCollection(descriptor)); + + var context = new ActionInvokerProviderContext(new ActionContext() + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); // Act - 1 invokerProvider.OnProvidersExecuting(context); @@ -363,21 +254,31 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal RelativePath = "Path1", FilterDescriptors = new FilterDescriptor[0], }; + var descriptorCollection1 = new ActionDescriptorCollection(new[] { descriptor }, version: 1); var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2); + var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.SetupSequence(p => p.ActionDescriptors) + actionDescriptorProvider + .SetupSequence(p => p.ActionDescriptors) .Returns(descriptorCollection1) .Returns(descriptorCollection2); var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor)); + var invokerProvider = CreateInvokerProvider( loader.Object, actionDescriptorProvider.Object); - var context = new ActionInvokerProviderContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + var context = new ActionInvokerProviderContext(new ActionContext() + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); // Act - 1 invokerProvider.OnProvidersExecuting(context); @@ -397,217 +298,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.NotSame(entry1, entry2); } - [Fact] - public void PopulateHandlerMethodDescriptors_DiscoversHandlersFromBaseType() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Deeper/Index.cshtml" - }; - - var actionDescriptor = CreateCompiledPageActionDescriptor(descriptor, typeof(InheritsMethods)); - - var type = actionDescriptor.ModelTypeInfo ?? actionDescriptor.PageTypeInfo; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(type, actionDescriptor); - - // Assert - Assert.Collection(actionDescriptor.HandlerMethods, - (handler) => - { - Assert.Equal("OnGet", handler.Method.Name); - Assert.Equal(typeof(InheritsMethods), handler.Method.DeclaringType); - }, - (handler) => - { - Assert.Equal("OnGet", handler.Method.Name); - Assert.Equal(typeof(TestSetPageModel), handler.Method.DeclaringType); - }, - (handler) => - { - Assert.Equal("OnPost", handler.Method.Name); - Assert.Equal(typeof(TestSetPageModel), handler.Method.DeclaringType); - }); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_IgnoresNonPublicMethods() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Deeper/Index.cshtml" - }; - - var actionDescriptor = CreateCompiledPageActionDescriptor(descriptor, typeof(ProtectedModel)); - - var type = actionDescriptor.ModelTypeInfo ?? actionDescriptor.PageTypeInfo; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(type, actionDescriptor); - - // Assert - Assert.Empty(actionDescriptor.HandlerMethods); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_IgnoreGenericTypeParameters() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Deeper/Index.cshtml" - }; - - var actionDescriptor = CreateCompiledPageActionDescriptor(descriptor, typeof(GenericClassModel)); - - var type = actionDescriptor.ModelTypeInfo ?? actionDescriptor.PageTypeInfo; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(type, actionDescriptor); - - // Assert - Assert.Empty(actionDescriptor.HandlerMethods); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_IgnoresStaticMethods() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Index.cshtml" - }; - - var modelTypeInfo = typeof(PageModelWithStaticHandler).GetTypeInfo(); - var expected = modelTypeInfo.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance); - var actionDescriptor = new CompiledPageActionDescriptor(descriptor) - { - ModelTypeInfo = modelTypeInfo, - PageTypeInfo = typeof(object).GetTypeInfo(), - }; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor); - - // Assert - Assert.Collection(actionDescriptor.HandlerMethods, - handler => Assert.Same(expected, handler.Method)); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_IgnoresAbstractMethods() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Index.cshtml" - }; - - var modelTypeInfo = typeof(PageModelWithAbstractMethod).GetTypeInfo(); - var expected = modelTypeInfo.GetMethod(nameof(PageModelWithAbstractMethod.OnGet)); - var actionDescriptor = new CompiledPageActionDescriptor(descriptor) - { - ModelTypeInfo = modelTypeInfo, - PageTypeInfo = typeof(object).GetTypeInfo(), - }; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor); - - // Assert - Assert.Collection(actionDescriptor.HandlerMethods, - handler => Assert.Same(expected, handler.Method)); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_DiscoversMethodsWithFormActions() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Index.cshtml" - }; - - var modelTypeInfo = typeof(PageModelWithFormActions).GetTypeInfo(); - var actionDescriptor = new CompiledPageActionDescriptor(descriptor) - { - ModelTypeInfo = modelTypeInfo, - PageTypeInfo = typeof(object).GetTypeInfo(), - }; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor); - - // Assert - Assert.Collection(actionDescriptor.HandlerMethods.OrderBy(h => h.Method.Name), - handler => - { - Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnGet)), handler.Method); - Assert.Equal("GET", handler.HttpMethod); - Assert.Equal(0, handler.FormAction.Length); - Assert.NotNull(handler.Executor); - }, - handler => - { - Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostAdd)), handler.Method); - Assert.Equal("POST", handler.HttpMethod); - Assert.Equal("Add", handler.FormAction.ToString()); - Assert.NotNull(handler.Executor); - }, - handler => - { - Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostAddCustomer)), handler.Method); - Assert.Equal("POST", handler.HttpMethod); - Assert.Equal("AddCustomer", handler.FormAction.ToString()); - Assert.NotNull(handler.Executor); - }, - handler => - { - Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostDeleteAsync)), handler.Method); - Assert.Equal("POST", handler.HttpMethod); - Assert.Equal("Delete", handler.FormAction.ToString()); - Assert.NotNull(handler.Executor); - }); - } - - [Fact] - public void PopulateHandlerMethodDescriptors_AllowOnlyOneMethod() - { - // Arrange - var descriptor = new PageActionDescriptor() - { - RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], - ViewEnginePath = "/Views/Deeper/Index.cshtml" - }; - - var actionDescriptor = CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel)); - - var type = actionDescriptor.ModelTypeInfo ?? actionDescriptor.PageTypeInfo; - - // Act - PageActionInvokerProvider.PopulateHandlerMethodDescriptors(type, actionDescriptor); - - // Assert - var handler = Assert.Single(actionDescriptor.HandlerMethods); - Assert.Equal("OnGet", handler.Method.Name); - } - [Fact] public void GetViewStartFactories_FindsFullHeirarchy() { @@ -618,12 +308,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal FilterDescriptors = new FilterDescriptor[0], ViewEnginePath = "/Views/Deeper/Index.cshtml" }; + var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); var fileProvider = new TestFileProvider(); fileProvider.AddFile("/View/Deeper/Not_ViewStart.cshtml", "page content"); @@ -635,20 +324,24 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var razorProject = new TestRazorProject(fileProvider); var mock = new Mock(); - mock.Setup(p => p.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) + mock + .Setup(p => p.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new List())) .Verifiable(); - mock.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) + mock + .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new List())) .Verifiable(); - mock.Setup(p => p.CreateFactory("/_ViewStart.cshtml")) + mock + .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new List())) .Verifiable(); + var razorPageFactoryProvider = mock.Object; var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), pageProvider: null, modelProvider: null, razorPageFactoryProvider: razorPageFactoryProvider, @@ -675,16 +368,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal FilterDescriptors = new FilterDescriptor[0], ViewEnginePath = "/Pages/Level1/Level2/Index.cshtml" }; + var compiledPageDescriptor = new CompiledPageActionDescriptor(descriptor) { PageTypeInfo = typeof(object).GetTypeInfo(), }; + var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(compiledPageDescriptor); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); var fileProvider = new TestFileProvider(); fileProvider.AddFile("/_ViewStart.cshtml", "page content"); @@ -696,13 +389,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var razorProject = new TestRazorProject(fileProvider); var mock = new Mock(MockBehavior.Strict); - mock.Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml")) + mock + .Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new List())) .Verifiable(); - mock.Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml")) + mock + .Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new List())) .Verifiable(); var razorPageFactoryProvider = mock.Object; + var options = new RazorPagesOptions { RootDirectory = rootDirectory, @@ -710,7 +406,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), razorPageFactoryProvider: razorPageFactoryProvider, razorProject: razorProject, razorPagesOptions: options); @@ -734,19 +430,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal FilterDescriptors = new FilterDescriptor[0], ViewEnginePath = "/Views/Deeper/Index.cshtml" }; + var loader = new Mock(); - loader.Setup(l => l.Load(It.IsAny())) + loader + .Setup(l => l.Load(It.IsAny())) .Returns(CreateCompiledPageActionDescriptor(descriptor, typeof(TestPageModel))); - var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); - var actionDescriptorProvider = new Mock(); - actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); var pageFactory = new Mock(); - pageFactory.Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) + pageFactory + .Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); - pageFactory.Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml")) + pageFactory + .Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(new IChangeToken[0])); - pageFactory.Setup(f => f.CreateFactory("/_ViewStart.cshtml")) + pageFactory + .Setup(f => f.CreateFactory("/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); // No files @@ -755,7 +453,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, + CreateActionDescriptorCollection(descriptor), pageProvider: null, modelProvider: null, razorPageFactoryProvider: pageFactory.Object, @@ -770,14 +468,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(2, factories.Count); } - private IRazorPageFactoryProvider CreateRazorPageFactoryProvider() - { - var mock = new Mock(); - mock.Setup(p => p.CreateFactory(It.IsAny())) - .Returns(new RazorPageFactoryResult(() => null, new List())); - return mock.Object; - } - private static CompiledPageActionDescriptor CreateCompiledPageActionDescriptor( PageActionDescriptor descriptor, Type pageType = null) @@ -805,7 +495,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal RazorPagesOptions razorPagesOptions = null) { var tempDataFactory = new Mock(); - tempDataFactory.Setup(t => t.GetTempData(It.IsAny())) + tempDataFactory + .Setup(t => t.GetTempData(It.IsAny())) .Returns((HttpContext context) => new TempDataDictionary(context, Mock.Of())); if (razorProject == null) @@ -838,102 +529,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal NullLoggerFactory.Instance); } - private class GenericClassModel + private IActionDescriptorCollectionProvider CreateActionDescriptorCollection(PageActionDescriptor descriptor) { - public void OnGet() - { + var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var actionDescriptorProvider = new Mock(); + actionDescriptorProvider + .Setup(p => p.ActionDescriptors) + .Returns(descriptorCollection); - } - } - - private class TestSetPageWithModel - { - public TestSetPageModel Model { get; set; } - } - - private class InheritsMethods : TestSetPageModel - { - public new void OnGet() - { - - } - } - - private class PageModelWithStaticHandler - { - public static void OnGet(string name) - { - - } - - public void OnGet() - { - - } - } - - private abstract class PageModelWithAbstractMethod - { - public abstract void OnPost(string name); - - public void OnGet() - { - - } - } - - private class PageModelWithFormActions - { - public void OnGet() - { - - } - - public void OnPostAdd() - { - - } - - public void OnPostAddCustomer() - { - - } - - public void OnPostDeleteAsync() - { - - } - - protected void OnPostDelete() - { - - } - } - - private class ProtectedModel - { - protected void OnGet() - { - - } - - private void OnPost() - { - - } - } - - private class TestSetPageModel - { - public void OnGet() - { - - } - - public void OnPost() - { - - } + return actionDescriptorProvider.Object; } private class PageWithModel @@ -947,11 +551,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { } } - - private class PageWithBoundProperties - { - [ModelBinder] - public string Id { get; set; } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs index 1273c54b63..8ea8d6a64b 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs @@ -353,6 +353,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal null, null, null, + null, new FilterItem[0]); var invoker = CreateInvoker( new[] { filter1.Object, filter2.Object, filter3.Object }, @@ -408,6 +409,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal null, null, null, + null, new FilterItem[0]); var invoker = CreateInvoker( new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }, @@ -521,6 +523,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { ViewEnginePath = "/Index.cshtml", RelativePath = "/Index.cshtml", + HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(TestPage).GetTypeInfo(), PageTypeInfo = typeof(TestPage).GetTypeInfo(), }; @@ -607,6 +611,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal (c, model) => { (model as IDisposable)?.Dispose(); }, null, null, + null, new FilterItem[0]); var invoker = new PageActionInvoker( diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs index 60e60a1985..618385a4f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PagePropertyBinderFactoryTest.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Moq; using Xunit; @@ -149,20 +151,51 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public async Task ModelBinderFactory_BindsPropertiesOnPage() { // Arrange + var type = typeof(PageWithProperty).GetTypeInfo(); + var actionDescriptor = new CompiledPageActionDescriptor { - PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), + BoundProperties = new [] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageWithProperty.Id), + ParameterType = typeof(int), + Property = type.GetProperty(nameof(PageWithProperty.Id)), + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageWithProperty.RouteDifferentValue), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageWithProperty.RouteDifferentValue)), + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageWithProperty.PropertyWithNoValue), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageWithProperty.PropertyWithNoValue)), + } + }, + HandlerTypeInfo = type, + PageTypeInfo = type, }; + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var binder = new TestParameterBinder(new Dictionary { { nameof(PageWithProperty.Id), 10 }, { nameof(PageWithProperty.RouteDifferentValue), "route-value" } }); + var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var page = new PageWithProperty { - PageContext = new PageContext(), + PageContext = new PageContext() + { + HttpContext = new DefaultHttpContext(), + }, }; // Act @@ -172,56 +205,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(10, page.Id); Assert.Equal("route-value", page.RouteDifferentValue); Assert.Null(page.PropertyWithNoValue); - Assert.Collection(binder.Descriptors, - descriptor => - { - Assert.Equal(nameof(PageWithProperty.Id), descriptor.Name); - Assert.Null(descriptor.BindingInfo.BinderModelName); - Assert.Equal(BindingSource.Query, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(int), descriptor.ParameterType); - }, - descriptor => - { - Assert.Equal(nameof(PageWithProperty.RouteDifferentValue), descriptor.Name); - Assert.Equal("route-value", descriptor.BindingInfo.BinderModelName); - Assert.Equal(BindingSource.Path, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(string), descriptor.ParameterType); - }, - descriptor => - { - Assert.Equal(nameof(PageWithProperty.PropertyWithNoValue), descriptor.Name); - Assert.Null(descriptor.BindingInfo.BinderModelName); - Assert.Equal(BindingSource.Form, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(string), descriptor.ParameterType); - }); } [Fact] public async Task ModelBinderFactory_BindsPropertiesOnPageModel() { // Arrange + var type = typeof(PageModelWithProperty).GetTypeInfo(); + var actionDescriptor = new CompiledPageActionDescriptor { + BoundProperties = new[] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithProperty.Id), + ParameterType = typeof(int), + Property = type.GetProperty(nameof(PageModelWithProperty.Id)), + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithProperty.RouteDifferentValue), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithProperty.RouteDifferentValue)), + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithProperty.PropertyWithNoValue), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithProperty.PropertyWithNoValue)), + } + }, + + HandlerTypeInfo = typeof(PageModelWithProperty).GetTypeInfo(), PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), ModelTypeInfo = typeof(PageModelWithProperty).GetTypeInfo(), }; + var binder = new TestParameterBinder(new Dictionary { { nameof(PageModelWithProperty.Id), 10 }, { nameof(PageModelWithProperty.RouteDifferentValue), "route-value" } }); + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var page = new PageWithProperty { - PageContext = new PageContext(), + PageContext = new PageContext() + { + HttpContext = new DefaultHttpContext(), + } }; + var model = new PageModelWithProperty(); // Act @@ -235,123 +272,44 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(10, model.Id); Assert.Equal("route-value", model.RouteDifferentValue); Assert.Null(model.PropertyWithNoValue); + } - Assert.Collection(binder.Descriptors, - descriptor => + [Fact] + public async Task ModelBinderFactory_PreservesExistingValueIfModelBindingFailed() + { + // Arrange + var type = typeof(PageModelWithDefaultValue).GetTypeInfo(); + + var actionDescriptor = new CompiledPageActionDescriptor + { + BoundProperties = new[] { - Assert.Equal(nameof(PageModelWithProperty.Id), descriptor.Name); - Assert.Equal(BindingSource.Query, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(int), descriptor.ParameterType); + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithDefaultValue.PropertyWithDefaultValue), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithDefaultValue.PropertyWithDefaultValue)), + }, }, - descriptor => - { - Assert.Equal(nameof(PageModelWithProperty.RouteDifferentValue), descriptor.Name); - Assert.Equal("route-value", descriptor.BindingInfo.BinderModelName); - Assert.Equal(BindingSource.Path, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(string), descriptor.ParameterType); - }, - descriptor => - { - Assert.Equal(nameof(PageModelWithProperty.PropertyWithNoValue), descriptor.Name); - Assert.Null(descriptor.BindingInfo.BinderModelName); - Assert.Equal(BindingSource.Form, descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(string), descriptor.ParameterType); - }); - } - [Fact] - public async Task ModelBinderFactory_DiscoversBinderType() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { + HandlerTypeInfo = type, PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), - ModelTypeInfo = typeof(PageModelWithModelBinderAttribute).GetTypeInfo(), + ModelTypeInfo = type, }; - var expected = Guid.NewGuid(); - var binder = new TestParameterBinder(new Dictionary - { - { nameof(PageModelWithModelBinderAttribute.PropertyWithBinderType), expected }, - }); - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); - var page = new PageWithProperty - { - PageContext = new PageContext(), - }; - var model = new PageModelWithModelBinderAttribute(); - // Act - await factory(page, model); - - // Assert - Assert.Equal(expected, model.PropertyWithBinderType); - Assert.Collection(binder.Descriptors, - descriptor => - { - Assert.Equal(nameof(PageModelWithModelBinderAttribute.PropertyWithBinderType), descriptor.Name); - Assert.Equal(BindingSource.Custom, descriptor.BindingInfo.BindingSource); - Assert.Equal(typeof(DeclarativeSecurityAction), descriptor.BindingInfo.BinderType); - Assert.Null(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(Guid), descriptor.ParameterType); - }); - } - - [Fact] - public async Task ModelBinderFactory_DiscoversPropertyFilter() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), - ModelTypeInfo = typeof(PageModelWithPropertyFilterAttribute).GetTypeInfo(), - }; var binder = new TestParameterBinder(new Dictionary()); + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var page = new PageWithProperty { - PageContext = new PageContext(), - }; - var model = new PageModelWithPropertyFilterAttribute(); - - // Act - await factory(page, model); - - // Assert - Assert.Collection(binder.Descriptors, - descriptor => + PageContext = new PageContext() { - Assert.Equal(nameof(PageModelWithPropertyFilterAttribute.PropertyWithFilter), descriptor.Name); - Assert.Null(descriptor.BindingInfo.BindingSource); - Assert.Null(descriptor.BindingInfo.BinderType); - Assert.IsType(descriptor.BindingInfo.PropertyFilterProvider); - Assert.Equal(typeof(object), descriptor.ParameterType); - }); - } + HttpContext = new DefaultHttpContext(), + } + }; - [Fact] - public async Task ModelBinderFactory_UsesDefaultValueIfModelBindingFailed() - { - // Arrange - var actionDescriptor = new CompiledPageActionDescriptor - { - PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), - ModelTypeInfo = typeof(PageModelWithDefaultValue).GetTypeInfo(), - }; - var binder = new TestParameterBinder(new Dictionary()); - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); - var page = new PageWithProperty - { - PageContext = new PageContext(), - }; var model = new PageModelWithDefaultValue(); var defaultValue = model.PropertyWithDefaultValue; @@ -362,34 +320,125 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(defaultValue, model.PropertyWithDefaultValue); } - [Fact] - public async Task ModelBinderFactory_OverwritesDefaultValue() + [Theory] + [InlineData("Get")] + [InlineData("GET")] + [InlineData("gET")] + public async Task ModelBinderFactory_IgnoresPropertyWithoutSupportsGet_WhenRequestIsGet(string method) { // Arrange + var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo(); + var actionDescriptor = new CompiledPageActionDescriptor { + BoundProperties = new[] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithSupportsGetProperty.SupportsGet), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)), + SupportsGet = true, + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithSupportsGetProperty.Default), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.Default)), + }, + }, + + HandlerTypeInfo = type, PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), - ModelTypeInfo = typeof(PageModelWithDefaultValue).GetTypeInfo(), + ModelTypeInfo = type, }; - var expected = "not-default-value"; - var binder = new TestParameterBinder(new Dictionary + + var binder = new TestParameterBinder(new Dictionary() { - { nameof(PageModelWithDefaultValue.PropertyWithDefaultValue), expected }, + { "SupportsGet", "value" }, + { "Default", "ignored" }, }); + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + var page = new PageWithProperty { - PageContext = new PageContext(), + PageContext = new PageContext() + { + HttpContext = new DefaultHttpContext(), + } }; - var model = new PageModelWithDefaultValue(); - var defaultValue = model.PropertyWithDefaultValue; + + page.HttpContext.Request.Method = method; + + var model = new PageModelWithSupportsGetProperty(); // Act await factory(page, model); // Assert - Assert.Equal(expected, model.PropertyWithDefaultValue); + Assert.Equal("value", model.SupportsGet); + Assert.Null(model.Default); + } + + [Fact] + public async Task ModelBinderFactory_BindsPropertyWithoutSupportsGet_WhenRequestIsNotGet() + { + // Arrange + var type = typeof(PageModelWithSupportsGetProperty).GetTypeInfo(); + + var actionDescriptor = new CompiledPageActionDescriptor + { + BoundProperties = new[] + { + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithSupportsGetProperty.SupportsGet), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.SupportsGet)), + SupportsGet = true, + }, + new PageBoundPropertyDescriptor() + { + Name = nameof(PageModelWithSupportsGetProperty.Default), + ParameterType = typeof(string), + Property = type.GetProperty(nameof(PageModelWithSupportsGetProperty.Default)), + }, + }, + + HandlerTypeInfo = type, + PageTypeInfo = typeof(PageWithProperty).GetTypeInfo(), + ModelTypeInfo = type, + }; + + var binder = new TestParameterBinder(new Dictionary() + { + { "SupportsGet", "value" }, + { "Default", "value" }, + }); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var factory = PagePropertyBinderFactory.CreateBinder(binder, modelMetadataProvider, actionDescriptor); + + var page = new PageWithProperty + { + PageContext = new PageContext() + { + HttpContext = new DefaultHttpContext(), + } + }; + + page.HttpContext.Request.Method = "Post"; + + var model = new PageModelWithSupportsGetProperty(); + + // Act + await factory(page, model); + + // Assert + Assert.Equal("value", model.SupportsGet); + Assert.Equal("value", model.Default); } private class TestParameterBinder : ParameterBinder @@ -518,5 +567,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal [ModelBinder] public string PropertyWithDefaultValue { get; set; } = "Hello world"; } + + private class PageModelWithSupportsGetProperty + { + [BindProperty(SupportsGet = true)] + public string SupportsGet { get; set; } + + public string Default { get; set; } + } } } diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyWithGet.cshtml b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyWithGet.cshtml new file mode 100644 index 0000000000..9f9ef25452 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/BindPropertyWithGet.cshtml @@ -0,0 +1,9 @@ +@page +@using Microsoft.AspNetCore.Mvc.RazorPages + +@functions +{ + [BindProperty(SupportsGet=true)] + public int Value { get; set; } +} +

@Value

\ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PageModelWithPropertyBinding.cs b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PageModelWithPropertyBinding.cs index b482274be7..1d436d9e79 100644 --- a/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PageModelWithPropertyBinding.cs +++ b/test/WebSites/RazorPagesWebSite/Pages/PropertyBinding/PageModelWithPropertyBinding.cs @@ -13,5 +13,7 @@ namespace RazorPagesWebSite [FromRoute] public int Id { get; set; } + + public void OnGet() { } } }