// 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.Compilation; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class DefaultPageLoader : IPageLoader { private const string ModelPropertyName = "Model"; private readonly IViewCompilerProvider _viewCompilerProvider; public DefaultPageLoader(IViewCompilerProvider viewCompilerProvider) { _viewCompilerProvider = viewCompilerProvider; } private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) { var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); var viewDescriptor = compileTask.GetAwaiter().GetResult(); var viewAttribute = viewDescriptor.ViewAttribute; var pageAttribute = new RazorPageAttribute( viewAttribute.Path, viewAttribute.ViewType, routeTemplate: null); return CreateDescriptor(actionDescriptor, pageAttribute); } // Internal for unit testing internal static CompiledPageActionDescriptor CreateDescriptor( PageActionDescriptor actionDescriptor, RazorPageAttribute pageAttribute) { var pageType = pageAttribute.ViewType.GetTypeInfo(); // 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. var modelType = pageAttribute.ViewType.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) { handlerType = modelType; } else { handlerType = pageType; handlerMethods = CreateHandlerMethods(pageType); } var boundProperties = CreateBoundProperties(handlerType); return new CompiledPageActionDescriptor(actionDescriptor) { 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 handler)) { continue; } var parameters = CreateHandlerParameters(method); var handlerMethodDescriptor = new HandlerMethodDescriptor() { MethodInfo = method, Name = handler, 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(); } } }