Introduce IPageApplicationModelPartsProvider (#9066)

* Resolved  #6919. It might be required to rework unit tests.
This commit is contained in:
Martin Gubis 2019-04-23 23:37:03 +02:00 committed by Pranav K
parent 3fbf3ac791
commit 1d5d144c12
8 changed files with 356 additions and 260 deletions

View File

@ -18,6 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
void Apply(Microsoft.AspNetCore.Mvc.ApplicationModels.PageApplicationModel model);
}
public partial interface IPageApplicationModelPartsProvider
{
Microsoft.AspNetCore.Mvc.ApplicationModels.PageHandlerModel CreateHandlerModel(System.Reflection.MethodInfo method);
Microsoft.AspNetCore.Mvc.ApplicationModels.PageParameterModel CreateParameterModel(System.Reflection.ParameterInfo parameter);
Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel CreatePropertyModel(System.Reflection.PropertyInfo property);
bool IsHandler(System.Reflection.MethodInfo methodInfo);
}
public partial interface IPageApplicationModelProvider
{
int Order { get; }

View File

@ -0,0 +1,276 @@
// 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.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
internal class DefaultPageApplicationModelPartsProvider: IPageApplicationModelPartsProvider
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly Func<ActionContext, bool> _supportsAllRequests;
private readonly Func<ActionContext, bool> _supportsNonGetRequests;
public DefaultPageApplicationModelPartsProvider(IModelMetadataProvider modelMetadataProvider)
{
_modelMetadataProvider = modelMetadataProvider;
_supportsAllRequests = _ => true;
_supportsNonGetRequests = context => !HttpMethods.IsGet(context.HttpContext.Request.Method);
}
/// <summary>
/// Creates a <see cref="PageHandlerModel"/> for the specified <paramref name="method"/>.s
/// </summary>
/// <param name="method">The <see cref="MethodInfo"/>.</param>
/// <returns>The <see cref="PageHandlerModel"/>.</returns>
public 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;
}
/// <summary>
/// Creates a <see cref="PageParameterModel"/> for the specified <paramref name="parameter"/>.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/>.</param>
/// <returns>The <see cref="PageParameterModel"/>.</returns>
public PageParameterModel CreateParameterModel(ParameterInfo parameter)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
var attributes = parameter.GetCustomAttributes(inherit: true);
BindingInfo bindingInfo;
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase)
{
var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameter);
bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata);
}
else
{
bindingInfo = BindingInfo.GetBindingInfo(attributes);
}
return new PageParameterModel(parameter, attributes)
{
BindingInfo = bindingInfo,
ParameterName = parameter.Name,
};
}
/// <summary>
/// Creates a <see cref="PagePropertyModel"/> for the <paramref name="property"/>.
/// </summary>
/// <param name="property">The <see cref="PropertyInfo"/>.</param>
/// <returns>The <see cref="PagePropertyModel"/>.</returns>
public PagePropertyModel CreatePropertyModel(PropertyInfo property)
{
if (property == null)
{
throw new ArgumentNullException(nameof(property));
}
var propertyAttributes = property.GetCustomAttributes(inherit: true);
// BindingInfo for properties can be either specified by decorating the property with binding-specific attributes.
// ModelMetadata also adds information from the property's type and any configured IBindingMetadataProvider.
var propertyMetadata = _modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
var bindingInfo = BindingInfo.GetBindingInfo(propertyAttributes, propertyMetadata);
if (bindingInfo == null)
{
// Look for BindPropertiesAttribute on the handler type if no BindingInfo was inferred for the property.
// This allows a user to enable model binding on properties by decorating the controller type with BindPropertiesAttribute.
var declaringType = property.DeclaringType;
var bindPropertiesAttribute = declaringType.GetCustomAttribute<BindPropertiesAttribute>(inherit: true);
if (bindPropertiesAttribute != null)
{
var requestPredicate = bindPropertiesAttribute.SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
bindingInfo = new BindingInfo
{
RequestPredicate = requestPredicate,
};
}
}
var model = new PagePropertyModel(property, propertyAttributes)
{
PropertyName = property.Name,
BindingInfo = bindingInfo,
};
return model;
}
/// <summary>
/// Determines if the specified <paramref name="methodInfo"/> is a handler.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <returns><c>true</c> if the <paramref name="methodInfo"/> is a handler. Otherwise <c>false</c>.</returns>
/// <remarks>
/// Override this method to provide custom logic to determine which methods are considered handlers.
/// </remarks>
public 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;
}
// Overridden 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;
}
}
}

View File

@ -22,19 +22,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private readonly PageHandlerResultFilter _pageHandlerResultFilter = new PageHandlerResultFilter();
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly RazorPagesOptions _razorPagesOptions;
private readonly Func<ActionContext, bool> _supportsAllRequests;
private readonly Func<ActionContext, bool> _supportsNonGetRequests;
private readonly IPageApplicationModelPartsProvider _pageApplicationModelPartsProvider;
private readonly HandleOptionsRequestsPageFilter _handleOptionsRequestsFilter;
public DefaultPageApplicationModelProvider(
IModelMetadataProvider modelMetadataProvider,
IOptions<RazorPagesOptions> razorPagesOptions)
IOptions<RazorPagesOptions> razorPagesOptions,
IPageApplicationModelPartsProvider pageApplicationModelPartsProvider)
{
_modelMetadataProvider = modelMetadataProvider;
_razorPagesOptions = razorPagesOptions.Value;
_pageApplicationModelPartsProvider = pageApplicationModelPartsProvider;
_supportsAllRequests = _ => true;
_supportsNonGetRequests = context => !HttpMethods.IsGet(context.HttpContext.Request.Method);
_handleOptionsRequestsFilter = new HandleOptionsRequestsPageFilter();
}
@ -133,7 +132,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
for (var i = 0; i < properties.Length; i++)
{
var propertyModel = CreatePropertyModel(properties[i].Property);
var propertyModel = _pageApplicationModelPartsProvider.CreatePropertyModel(properties[i].Property);
if (propertyModel != null)
{
propertyModel.Page = pageModel;
@ -149,7 +148,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
for (var i = 0; i < methods.Length; i++)
{
var handler = CreateHandlerModel(methods[i]);
var handler = _pageApplicationModelPartsProvider.CreateHandlerModel(methods[i]);
if (handler != null)
{
pageModel.HandlerMethods.Add(handler);
@ -181,250 +180,5 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
pageModel.Filters.Add(_handleOptionsRequestsFilter);
}
/// <summary>
/// Creates a <see cref="PageHandlerModel"/> for the specified <paramref name="method"/>.s
/// </summary>
/// <param name="method">The <see cref="MethodInfo"/>.</param>
/// <returns>The <see cref="PageHandlerModel"/>.</returns>
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;
}
/// <summary>
/// Creates a <see cref="PageParameterModel"/> for the specified <paramref name="parameter"/>.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/>.</param>
/// <returns>The <see cref="PageParameterModel"/>.</returns>
protected virtual PageParameterModel CreateParameterModel(ParameterInfo parameter)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
var attributes = parameter.GetCustomAttributes(inherit: true);
BindingInfo bindingInfo;
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase)
{
var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameter);
bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata);
}
else
{
bindingInfo = BindingInfo.GetBindingInfo(attributes);
}
return new PageParameterModel(parameter, attributes)
{
BindingInfo = bindingInfo,
ParameterName = parameter.Name,
};
}
/// <summary>
/// Creates a <see cref="PagePropertyModel"/> for the <paramref name="property"/>.
/// </summary>
/// <param name="property">The <see cref="PropertyInfo"/>.</param>
/// <returns>The <see cref="PagePropertyModel"/>.</returns>
protected virtual PagePropertyModel CreatePropertyModel(PropertyInfo property)
{
if (property == null)
{
throw new ArgumentNullException(nameof(property));
}
var propertyAttributes = property.GetCustomAttributes(inherit: true);
// BindingInfo for properties can be either specified by decorating the property with binding-specific attributes.
// ModelMetadata also adds information from the property's type and any configured IBindingMetadataProvider.
var propertyMetadata = _modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name);
var bindingInfo = BindingInfo.GetBindingInfo(propertyAttributes, propertyMetadata);
if (bindingInfo == null)
{
// Look for BindPropertiesAttribute on the handler type if no BindingInfo was inferred for the property.
// This allows a user to enable model binding on properties by decorating the controller type with BindPropertiesAttribute.
var declaringType = property.DeclaringType;
var bindPropertiesAttribute = declaringType.GetCustomAttribute<BindPropertiesAttribute>(inherit: true);
if (bindPropertiesAttribute != null)
{
var requestPredicate = bindPropertiesAttribute.SupportsGet ? _supportsAllRequests : _supportsNonGetRequests;
bindingInfo = new BindingInfo
{
RequestPredicate = requestPredicate,
};
}
}
var model = new PagePropertyModel(property, propertyAttributes)
{
PropertyName = property.Name,
BindingInfo = bindingInfo,
};
return model;
}
/// <summary>
/// Determines if the specified <paramref name="methodInfo"/> is a handler.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <returns><c>true</c> if the <paramref name="methodInfo"/> is a handler. Otherwise <c>false</c>.</returns>
/// <remarks>
/// Override this method to provide custom logic to determine which methods are considered handlers.
/// </remarks>
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;
}
// Overridden 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;
}
}
}

View File

@ -0,0 +1,48 @@
// 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.Text;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
/// <summary>
/// Provides parts that are used to construct a <see cref="PageApplicationModel" /> instance
/// </summary>
public interface IPageApplicationModelPartsProvider
{
/// <summary>
/// Creates a <see cref="PageHandlerModel"/> for the specified <paramref name="method"/>.s
/// </summary>
/// <param name="method">The <see cref="MethodInfo"/>.</param>
/// <returns>The <see cref="PageHandlerModel"/>.</returns>
PageHandlerModel CreateHandlerModel(MethodInfo method);
/// <summary>
/// Creates a <see cref="PageParameterModel"/> for the specified <paramref name="parameter"/>.
/// </summary>
/// <param name="parameter">The <see cref="ParameterInfo"/>.</param>
/// <returns>The <see cref="PageParameterModel"/>.</returns>
PageParameterModel CreateParameterModel(ParameterInfo parameter);
/// <summary>
/// Creates a <see cref="PagePropertyModel"/> for the <paramref name="property"/>.
/// </summary>
/// <param name="property">The <see cref="PropertyInfo"/>.</param>
/// <returns>The <see cref="PagePropertyModel"/>.</returns>
PagePropertyModel CreatePropertyModel(PropertyInfo property);
/// <summary>
/// Determines if the specified <paramref name="methodInfo"/> is a handler.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <returns><c>true</c> if the <paramref name="methodInfo"/> is a handler. Otherwise <c>false</c>.</returns>
/// <remarks>
/// Override this method to provide custom logic to determine which methods are considered handlers.
/// </remarks>
bool IsHandler(MethodInfo methodInfo);
}
}

View File

@ -113,6 +113,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, ResponseCacheFilterApplicationModelProvider>());
services.TryAddSingleton<IPageApplicationModelPartsProvider, DefaultPageApplicationModelPartsProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionInvokerProvider, PageActionInvokerProvider>());

View File

@ -188,9 +188,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private static PageApplicationModelProviderContext GetApplicationProviderContext(TypeInfo typeInfo)
{
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var defaultProvider = new DefaultPageApplicationModelProvider(
TestModelMetadataProvider.CreateDefaultProvider(),
Options.Create(new RazorPagesOptions()));
modelMetadataProvider,
Options.Create(new RazorPagesOptions()),
new DefaultPageApplicationModelPartsProvider(modelMetadataProvider));
var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);
defaultProvider.OnProvidersExecuting(context);

View File

@ -971,7 +971,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName)
{
// Act
var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);
var result = DefaultPageApplicationModelPartsProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);
// Assert
Assert.False(result);
@ -993,7 +993,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
// Arrange
// Act
var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);
var result = DefaultPageApplicationModelPartsProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler);
// Assert
Assert.True(result);
@ -1169,9 +1169,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
private static DefaultPageApplicationModelProvider CreateProvider()
{
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
return new DefaultPageApplicationModelProvider(
TestModelMetadataProvider.CreateDefaultProvider(),
Options.Create(new RazorPagesOptions()));
modelMetadataProvider,
Options.Create(new RazorPagesOptions()),
new DefaultPageApplicationModelPartsProvider(modelMetadataProvider));
}
}
}

View File

@ -142,9 +142,12 @@ namespace Microsoft.AspNetCore.Mvc.Filters
private static PageApplicationModelProviderContext GetApplicationProviderContext(TypeInfo typeInfo)
{
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var defaultProvider = new DefaultPageApplicationModelProvider(
TestModelMetadataProvider.CreateDefaultProvider(),
Options.Create(new RazorPagesOptions()));
modelMetadataProvider,
Options.Create(new RazorPagesOptions()),
new DefaultPageApplicationModelPartsProvider(modelMetadataProvider));
var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo);
defaultProvider.OnProvidersExecuting(context);