From 4faef7afafceb307be4e5f56b5ad4e91200ed46e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 28 Feb 2017 17:42:24 -0800 Subject: [PATCH] Cache more things in HandlerMethodDescriptor Add tests for DefaultPageHandlerMethodSelector --- .../Infrastructure/HandlerMethodDescriptor.cs | 5 + .../DefaultPageHandlerMethodSelector.cs | 136 +++--- .../Internal/PageActionInvokerProvider.cs | 92 +++- .../Properties/Resources.Designer.cs | 16 + .../Resources.resx | 3 + .../DefaultPageHandlerMethodSelectorTest.cs | 406 ++++++++++++++++++ .../Internal/PageActionInvokerProviderTest.cs | 166 ++++++- 7 files changed, 727 insertions(+), 97 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs index 4a5bcc974e..b19700dad6 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs @@ -4,6 +4,7 @@ using System; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { @@ -12,5 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public MethodInfo Method { get; set; } public Func> Executor { get; set; } + + public string HttpMethod { get; set; } + + public StringSegment FormAction { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs index 3092ce2738..725bdee934 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs @@ -3,140 +3,106 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageHandlerMethodSelector : IPageHandlerMethodSelector { + private const string FormAction = "formaction"; + public HandlerMethodDescriptor Select(PageContext context) { - var handlers = new List(context.ActionDescriptor.HandlerMethods.Count); - for (var i = 0; i < context.ActionDescriptor.HandlerMethods.Count; i++) + var handlers = SelectHandlers(context); + if (handlers == null || handlers.Count == 0) { - handlers.Add(HandlerMethodAndMetadata.Create(context.ActionDescriptor.HandlerMethods[i])); + return null; } - for (var i = handlers.Count - 1; i >= 0; i--) + List ambiguousMatches = null; + HandlerMethodDescriptor bestMatch = null; + for (var score = 2; score >= 0; score--) { - var handler = handlers[i]; - - if (handler.HttpMethod != null && - !string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + for (var i = 0; i < handlers.Count; i++) { - handlers.RemoveAt(i); - } - } - - var formaction = Convert.ToString(context.RouteData.Values["formaction"]); - - for (var i = handlers.Count - 1; i >= 0; i--) - { - var handler = handlers[i]; - - if (handler.Formaction != null && - !string.Equals(handler.Formaction, formaction, StringComparison.OrdinalIgnoreCase)) - { - handlers.RemoveAt(i); - } - } - - var ambiguousMatches = (List)null; - var best = (HandlerMethodAndMetadata?)null; - for (var i = 2; i >= 0; i--) - { - for (var j = 0; j < handlers.Count; j++) - { - var handler = handlers[j]; - if (handler.GetScore() == i) + var handler = handlers[i]; + if (GetScore(handler) == score) { - if (best == null) + if (bestMatch == null) { - best = handler; + bestMatch = handler; continue; } if (ambiguousMatches == null) { ambiguousMatches = new List(); - ambiguousMatches.Add(best.Value.Handler); + ambiguousMatches.Add(bestMatch); } - ambiguousMatches.Add(handler.Handler); + ambiguousMatches.Add(handler); } } if (ambiguousMatches != null) { - throw new InvalidOperationException($"Selecting a handler is ambiguous! Matches: {string.Join(", ", ambiguousMatches)}"); + var ambiguousMethods = string.Join(", ", ambiguousMatches.Select(m => m.Method)); + throw new InvalidOperationException(Resources.FormatAmbiguousHandler(Environment.NewLine, ambiguousMethods)); } - if (best != null) + if (bestMatch != null) { - return best.Value.Handler; + return bestMatch; } } return null; } - // Bad prototype substring implementation :) - private struct HandlerMethodAndMetadata + private static List SelectHandlers(PageContext context) { - public static HandlerMethodAndMetadata Create(HandlerMethodDescriptor handler) + var handlers = context.ActionDescriptor.HandlerMethods; + List handlersToConsider = null; + + var formAction = Convert.ToString(context.RouteData.Values[FormAction]); + for (var i = 0; i < handlers.Count; i++) { - var name = handler.Method.Name; - - string httpMethod; - if (name.StartsWith("OnGet", StringComparison.Ordinal)) + var handler = handlers[i]; + if (handler.HttpMethod != null && + !string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) { - httpMethod = "GET"; + continue; } - else if (name.StartsWith("OnPost", StringComparison.Ordinal)) + else if (handler.FormAction.HasValue && + !handler.FormAction.Equals(formAction, StringComparison.OrdinalIgnoreCase)) { - httpMethod = "POST"; - } - else - { - httpMethod = null; + continue; } - var formactionStart = httpMethod?.Length + 2 ?? 0; - var formactionLength = name.EndsWith("Async", StringComparison.Ordinal) - ? name.Length - formactionStart - "Async".Length - : name.Length - formactionStart; + if (handlersToConsider == null) + { + handlersToConsider = new List(); + } - var formaction = formactionLength == 0 ? null : name.Substring(formactionStart, formactionLength); - - return new HandlerMethodAndMetadata(handler, httpMethod, formaction); + handlersToConsider.Add(handler); } - public HandlerMethodAndMetadata(HandlerMethodDescriptor handler, string httpMethod, string formaction) + return handlersToConsider; + } + + private static int GetScore(HandlerMethodDescriptor descriptor) + { + if (descriptor.FormAction != null) { - Handler = handler; - HttpMethod = httpMethod; - Formaction = formaction; + return 2; } - - public HandlerMethodDescriptor Handler { get; } - - public string HttpMethod { get; } - - public string Formaction { get; } - - public int GetScore() + else if (descriptor.HttpMethod != null) { - if (Formaction != null) - { - return 2; - } - else if (HttpMethod != null) - { - return 1; - } - else - { - return 0; - } + return 1; + } + else + { + return 0; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 325d1ab721..f13d7edc44 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -18,12 +18,12 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageActionInvokerProvider : IActionInvokerProvider { - private static readonly string[] _handlerMethodNames = new string[] { "OnGet", "OnPost" }; private const string PageStartFileName = "_PageStart.cshtml"; private readonly IPageLoader _loader; private readonly IPageFactoryProvider _pageFactoryProvider; @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageStartItems = _razorProject.FindHierarchicalItems(descriptor.ViewEnginePath, PageStartFileName); foreach (var item in pageStartItems) { - if(item.Exists) + if (item.Exists) { var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path); if (factoryResult.Success) @@ -220,21 +220,91 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Internal for testing. internal static void PopulateHandlerMethodDescriptors(TypeInfo type, CompiledPageActionDescriptor actionDescriptor) { - for (var i = 0; i < _handlerMethodNames.Length; i++) + var methods = type.GetMethods(); + for (var i = 0; i < methods.Length; i++) { - var methodName = _handlerMethodNames[i]; - var method = type.GetMethod(methodName); - if (method != null && !method.IsGenericMethod) + var method = methods[i]; + if (!IsValidHandler(method)) { - actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor() - { - Method = method, - Executor = ExecutorFactory.CreateExecutor(actionDescriptor, 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 handlerMethodDescriptor = new HandlerMethodDescriptor + { + Method = method, + Executor = ExecutorFactory.CreateExecutor(actionDescriptor, method), + FormAction = formAction, + HttpMethod = httpMethod, + }; + + actionDescriptor.HandlerMethods.Add(handlerMethodDescriptor); } } + private static bool IsValidHandler(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 class InnerCache { public InnerCache(int version) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 4c5ab25565..c74d002493 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -106,6 +106,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedHandlerMethodType"), p0); } + /// + /// Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1} + /// + internal static string AmbiguousHandler + { + get { return GetString("AmbiguousHandler"); } + } + + /// + /// Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1} + /// + internal static string FormatAmbiguousHandler(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousHandler"), p0, p1); + } + /// /// Path must be an application relative path that starts with a forward slash '/'. /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index 95d5df2bae..2e8f00040c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -135,6 +135,9 @@ Unsupported handler method return type '{0}'. + + Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1} + Path must be an application relative path that starts with a forward slash '/'. diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs new file mode 100644 index 0000000000..dd739f0170 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs @@ -0,0 +1,406 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageHandlerMethodSelectorTest + { + [Fact] + public void Select_ReturnsNull_WhenNoHandlerMatchesHttpMethod() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "GET" + }; + + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = "POST" + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData(), + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "PUT" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void Select_ReturnsOnlyHandler() + { + // Arrange + var descriptor = new HandlerMethodDescriptor + { + HttpMethod = "GET" + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor, + }, + }, + RouteData = new RouteData(), + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "GET" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Same(descriptor, actual); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + public void Select_ReturnsHandlerWithMatchingHttpRequestMethod(string httpMethod) + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "PUT", + }; + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = httpMethod, + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData(), + HttpContext = new DefaultHttpContext + { + Request = + { + Method = httpMethod, + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Same(descriptor2, actual); + } + + [Fact] + public void Select_ReturnsNullWhenNoHandlerMatchesFormAction() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + FormAction = new StringSegment("Add"), + }; + + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + FormAction = new StringSegment("Delete"), + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData + { + Values = + { + { "formaction", "update" } + } + }, + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "POST" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void Select_ReturnsHandlerThatMatchesFormAction() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + FormAction = new StringSegment("Add"), + }; + + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + FormAction = new StringSegment("Delete"), + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData + { + Values = + { + { "formaction", "Add" } + } + }, + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "Post" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Same(descriptor1, actual); + } + + [Fact] + public void Select_ReturnsHandlerWithMatchingHttpMethodWithoutAFormAction() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + FormAction = new StringSegment("Subscribe"), + }; + + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData + { + Values = + { + { "formaction", "Add" } + } + }, + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "Post" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Same(descriptor2, actual); + } + + [Fact] + public void Select_WithoutFormAction_ThrowsIfMoreThanOneHandlerMatches() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + Method = GetType().GetMethod(nameof(Post)), + HttpMethod = "POST", + }; + + var descriptor2 = new HandlerMethodDescriptor + { + Method = GetType().GetMethod(nameof(PostAsync)), + HttpMethod = "POST", + }; + + var descriptor3 = new HandlerMethodDescriptor + { + HttpMethod = "GET", + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + descriptor3, + }, + }, + RouteData = new RouteData(), + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "Post" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act & Assert + var ex = Assert.Throws(() => selector.Select(pageContext)); + var methods = descriptor1.Method + ", " + descriptor2.Method; + var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" + + Environment.NewLine + Environment.NewLine + methods; + + Assert.Equal(message, ex.Message); + } + + [Fact] + public void Select_WithFormAction_ThrowsIfMoreThanOneHandlerMatches() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + Method = GetType().GetMethod(nameof(Post)), + HttpMethod = "POST", + FormAction = new StringSegment("Add"), + }; + + var descriptor2 = new HandlerMethodDescriptor + { + Method = GetType().GetMethod(nameof(PostAsync)), + HttpMethod = "POST", + FormAction = new StringSegment("Add"), + }; + + var descriptor3 = new HandlerMethodDescriptor + { + HttpMethod = "GET", + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = + { + descriptor1, + descriptor2, + descriptor3, + }, + }, + RouteData = new RouteData + { + Values = + { + { "formaction", "Add" } + } + }, + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "Post" + }, + }, + }; + var selector = new DefaultPageHandlerMethodSelector(); + + // Act & Assert + var ex = Assert.Throws(() => selector.Select(pageContext)); + var methods = descriptor1.Method + ", " + descriptor2.Method; + var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" + + Environment.NewLine + Environment.NewLine + methods; + + Assert.Equal(message, ex.Message); + } + + public void Post() + { + } + + public void PostAsync() + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 4ca9a13b62..2007250104 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -382,6 +383,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal 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); @@ -389,7 +395,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void PopulateHandlerMethodDescriptors_ProtectedMethodsNotFound() + public void PopulateHandlerMethodDescriptors_IgnoresNonPublicMethods() { // Arrange var descriptor = new PageActionDescriptor() @@ -432,6 +438,113 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal 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() { @@ -635,6 +748,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } + 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()