aspnetcore/src/Microsoft.AspNetCore.Mvc.Ra.../Internal/DefaultPageLoader.cs

284 lines
10 KiB
C#

// 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<HandlerMethodDescriptor>();
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<BindPropertyAttribute>();
var results = new List<PageBoundPropertyDescriptor>();
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<BindPropertyAttribute>();
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();
}
}
}