// 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.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class DefaultPageApplicationModelProvider : IPageApplicationModelProvider { private const string ModelPropertyName = "Model"; /// public int Order => -1000; /// public void OnProvidersExecuting(PageApplicationModelProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } context.PageApplicationModel = CreateModel(context.ActionDescriptor, context.PageType); } /// public void OnProvidersExecuted(PageApplicationModelProviderContext context) { } /// /// Creates a for the given . /// /// The . /// The . /// A for the given . protected virtual PageApplicationModel CreateModel( PageActionDescriptor actionDescriptor, TypeInfo pageTypeInfo) { if (actionDescriptor == null) { throw new ArgumentNullException(nameof(actionDescriptor)); } if (pageTypeInfo == null) { throw new ArgumentNullException(nameof(pageTypeInfo)); } if (!typeof(PageBase).GetTypeInfo().IsAssignableFrom(pageTypeInfo)) { throw new InvalidOperationException(Resources.FormatInvalidPageType_WrongBase( pageTypeInfo.FullName, typeof(PageBase).FullName)); } // 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 modelProperty = pageTypeInfo.GetProperty(ModelPropertyName, BindingFlags.Public | BindingFlags.Instance); if (modelProperty == null) { throw new InvalidOperationException(Resources.FormatInvalidPageType_NoModelProperty( pageTypeInfo.FullName, ModelPropertyName)); } var modelTypeInfo = modelProperty.PropertyType.GetTypeInfo(); // Now we want figure out which type is the handler type. TypeInfo handlerType; if (modelProperty.PropertyType.IsDefined(typeof(PageModelAttribute), inherit: true)) { handlerType = modelTypeInfo; } else { handlerType = pageTypeInfo; } var handlerTypeAttributes = handlerType.GetCustomAttributes(inherit: true); var pageModel = new PageApplicationModel( actionDescriptor, handlerType, handlerTypeAttributes) { PageType = pageTypeInfo, ModelType = modelTypeInfo, }; PopulateHandlerMethods(pageModel); PopulateHandlerProperties(pageModel); PopulateFilters(pageModel); return pageModel; } // Internal for unit testing internal void PopulateHandlerProperties(PageApplicationModel pageModel) { var properties = PropertyHelper.GetVisibleProperties(pageModel.HandlerType.AsType()); for (var i = 0; i < properties.Length; i++) { var propertyModel = CreatePropertyModel(properties[i].Property); if (propertyModel != null) { propertyModel.Page = pageModel; pageModel.HandlerProperties.Add(propertyModel); } } } // Internal for unit testing internal void PopulateHandlerMethods(PageApplicationModel pageModel) { var methods = pageModel.HandlerType.GetMethods(); for (var i = 0; i < methods.Length; i++) { var handler = CreateHandlerModel(methods[i]); if (handler != null) { pageModel.HandlerMethods.Add(handler); } } } internal void PopulateFilters(PageApplicationModel pageModel) { for (var i = 0; i < pageModel.HandlerTypeAttributes.Count; i++) { if (pageModel.HandlerTypeAttributes[i] is IFilterMetadata filter) { pageModel.Filters.Add(filter); } } } /// /// Creates a for the specified .s /// /// The . /// The . protected virtual PageHandlerModel CreateHandlerModel(MethodInfo method) { if (method == null) { throw new ArgumentNullException(nameof(method)); } if (!IsHandler(method)) { return null; } if (!TryParseHandlerMethod(method.Name, out var httpMethod, out var handlerName)) { return null; } var handlerModel = new PageHandlerModel( method, method.GetCustomAttributes(inherit: true)) { Name = method.Name, HandlerName = handlerName, HttpMethod = httpMethod, }; var methodParameters = handlerModel.MethodInfo.GetParameters(); for (var i = 0; i < methodParameters.Length; i++) { var parameter = methodParameters[i]; var parameterModel = CreateParameterModel(parameter); parameterModel.Handler = handlerModel; handlerModel.Parameters.Add(parameterModel); } return handlerModel; } /// /// Creates a for the specified . /// /// The . /// The . protected virtual PageParameterModel CreateParameterModel(ParameterInfo parameter) { if (parameter == null) { throw new ArgumentNullException(nameof(parameter)); } return new PageParameterModel(parameter, parameter.GetCustomAttributes(inherit: true)) { BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes()), ParameterName = parameter.Name, }; } /// /// Creates a for the . /// /// The . /// The . protected virtual PagePropertyModel CreatePropertyModel(PropertyInfo property) { if (property == null) { throw new ArgumentNullException(nameof(property)); } var attributes = property.GetCustomAttributes(inherit: true); var bindingInfo = BindingInfo.GetBindingInfo(attributes); var model = new PagePropertyModel(property, property.GetCustomAttributes(inherit: true)) { PropertyName = property.Name, BindingInfo = bindingInfo, }; return model; } /// /// Determines if the specified is a handler. /// /// The . /// true if the is a handler. Otherwise false. /// /// Override this method to provide custom logic to determine which methods are considered handlers. /// protected virtual bool IsHandler(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; } if (!methodInfo.IsPublic) { return false; } if (methodInfo.IsDefined(typeof(NonHandlerAttribute))) { return false; } // Exclude the whole hierarchy of Page. var declaringType = methodInfo.DeclaringType; if (declaringType == typeof(Page) || declaringType == typeof(PageBase) || declaringType == typeof(RazorPageBase)) { return false; } // Exclude methods declared on PageModel if (declaringType == typeof(PageModel)) { return false; } return true; } 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; } } }