From de30c5822a19f73672897db59fe20253cd3f9f29 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 22 Feb 2017 16:01:27 -0800 Subject: [PATCH] * Cache ExecutorFactory as part of CompiledActionDescriptor * Add tests for ExecutorFactory --- .../Properties/Resources.Designer.cs | 64 +-- .../Infrastructure/HandlerMethodDescriptor.cs | 4 + .../Internal/ExecutorFactory.cs | 137 +++--- .../Internal/PageActionInvoker.cs | 2 +- .../Internal/PageActionInvokerProvider.cs | 1 + .../Properties/Resources.Designer.cs | 16 + .../Resources.resx | 3 + .../project.json | 4 + .../Internal/ExecutorFactoryTest.cs | 454 ++++++++++++++++++ .../Internal/PageActionInvokerProviderTest.cs | 54 ++- 10 files changed, 646 insertions(+), 93 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs index 3cd4baf115..17436ce59e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs @@ -170,38 +170,6 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions return GetString("BindingSource_Services"); } - /// - /// Special - /// - internal static string BindingSource_Special - { - get { return GetString("BindingSource_Special"); } - } - - /// - /// Special - /// - internal static string FormatBindingSource_Special() - { - return GetString("BindingSource_Special"); - } - - /// - /// FormFile - /// - internal static string BindingSource_FormFile - { - get { return GetString("BindingSource_FormFile"); } - } - - /// - /// FormFile - /// - internal static string FormatBindingSource_FormFile() - { - return GetString("BindingSource_FormFile"); - } - /// /// ModelBinding /// @@ -314,6 +282,38 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1); } + /// + /// Special + /// + internal static string BindingSource_Special + { + get { return GetString("BindingSource_Special"); } + } + + /// + /// Special + /// + internal static string FormatBindingSource_Special() + { + return GetString("BindingSource_Special"); + } + + /// + /// FormFile + /// + internal static string BindingSource_FormFile + { + get { return GetString("BindingSource_FormFile"); } + } + + /// + /// FormFile + /// + internal static string FormatBindingSource_FormFile() + { + return GetString("BindingSource_FormFile"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs index 1a0f47ce8d..4a5bcc974e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs @@ -1,12 +1,16 @@ // 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 System.Threading.Tasks; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class HandlerMethodDescriptor { public MethodInfo Method { get; set; } + + public Func> Executor { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs index 1a535a0e90..5aa4593d0d 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs @@ -5,29 +5,31 @@ using System; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public static class ExecutorFactory { - public static Func> Create(MethodInfo method) + public static Func> CreateExecutor( + CompiledPageActionDescriptor actionDescriptor, + MethodInfo method) { - return new Executor() + if (actionDescriptor == null) { - Method = method, - }.Execute; - } + throw new ArgumentNullException(nameof(actionDescriptor)); + } - private class Executor - { - public MethodInfo Method { get; set; } - - public async Task Execute(Page page, object model) + if (method == null) { - var handler = HandlerMethod.Create(Method); + throw new ArgumentNullException(nameof(method)); + } - var receiver = Method.DeclaringType.IsAssignableFrom(page.GetType()) ? page : model; + var methodIsDeclaredOnPage = method.DeclaringType.GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo); + var handler = CreateHandlerMethod(method); + return async (page, model) => + { var arguments = new object[handler.Parameters.Length]; for (var i = 0; i < handler.Parameters.Length; i++) { @@ -39,68 +41,69 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal parameter.Name); } + var receiver = methodIsDeclaredOnPage ? page : model; var result = await handler.Execute(receiver, arguments); return result; - } + }; } - private class HandlerParameter + private static HandlerMethod CreateHandlerMethod(MethodInfo method) { - public string Name { get; set; } + var methodParameters = method.GetParameters(); + var parameters = new HandlerParameter[methodParameters.Length]; - public Type Type { get; set; } + for (var i = 0; i < methodParameters.Length; i++) + { + var methodParameter = methodParameters[i]; + object defaultValue = null; + if (methodParameter.HasDefaultValue) + { + defaultValue = methodParameter.DefaultValue; + } + else if (methodParameter.ParameterType.GetTypeInfo().IsValueType) + { + defaultValue = Activator.CreateInstance(methodParameter.ParameterType); + } - public object DefaultValue { get; set; } + parameters[i] = new HandlerParameter(methodParameter.Name, methodParameter.ParameterType, defaultValue); + } + + var returnType = method.ReturnType; + var returnTypeInfo = method.ReturnType.GetTypeInfo(); + if (returnType == typeof(void)) + { + return new VoidHandlerMethod(parameters, method); + } + else if (typeof(IActionResult).IsAssignableFrom(returnType)) + { + return new ActionResultHandlerMethod(parameters, method); + } + else if (returnType == typeof(Task)) + { + return new NonGenericTaskHandlerMethod(parameters, method); + } + else + { + var taskType = ClosedGenericMatcher.ExtractGenericInterface(returnType, typeof(Task<>)); + if (taskType != null && typeof(IActionResult).IsAssignableFrom(taskType.GenericTypeArguments[0])) + { + return new GenericTaskHandlerMethod(parameters, method); + } + } + + throw new InvalidOperationException(Resources.FormatUnsupportedHandlerMethodType(returnType)); } private abstract class HandlerMethod { - public static HandlerMethod Create(MethodInfo method) - { - var methodParameters = method.GetParameters(); - var parameters = new HandlerParameter[methodParameters.Length]; - - for (var i = 0; i < methodParameters.Length; i++) - { - parameters[i] = new HandlerParameter() - { - DefaultValue = methodParameters[i].HasDefaultValue ? methodParameters[i].DefaultValue : null, - Name = methodParameters[i].Name, - Type = methodParameters[i].ParameterType, - }; - } - - if (method.ReturnType == typeof(Task)) - { - return new NonGenericTaskHandlerMethod(parameters, method); - } - else if (method.ReturnType == typeof(void)) - { - return new VoidHandlerMethod(parameters, method); - } - else if ( - method.ReturnType.IsConstructedGenericType && - method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) && - typeof(IActionResult).IsAssignableFrom(method.ReturnType.GetTypeInfo().GetGenericArguments()[0])) - { - return new GenericTaskHandlerMethod(parameters, method); - } - else if (typeof(IActionResult).IsAssignableFrom(method.ReturnType)) - { - return new ActionResultHandlerMethod(parameters, method); - } - else - { - throw new InvalidOperationException("unsupported handler method return type"); - } - } - protected static Expression[] Unpack(Expression arguments, HandlerParameter[] parameters) { var unpackExpressions = new Expression[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { - unpackExpressions[i] = Expression.Convert(Expression.ArrayIndex(arguments, Expression.Constant(i)), parameters[i].Type); + unpackExpressions[i] = Expression.Convert( + Expression.ArrayIndex(arguments, Expression.Constant(i)), + parameters[i].Type); } return unpackExpressions; @@ -178,7 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private static async Task Convert(object taskAsObject) { var task = (Task)taskAsObject; - return (object)await task; + return await task; } } @@ -234,5 +237,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return Task.FromResult(_thunk(receiver, arguments)); } } + + private struct HandlerParameter + { + public HandlerParameter(string name, Type type, object defaultValue) + { + Name = name; + Type = type; + DefaultValue = defaultValue; + } + + public string Name { get; } + + public Type Type { get; } + + public object DefaultValue { get; } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 2db01fdc7d..9810871212 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -365,7 +365,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var handler = _selector.Select(_pageContext); if (handler != null) { - var executor = ExecutorFactory.Create(handler.Method); + var executor = handler.Executor; result = await executor(_page, _model); } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 442cc7f023..47ec269dda 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -239,6 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor() { Method = method, + Executor = ExecutorFactory.CreateExecutor(actionDescriptor, method), }); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 365bdcc9e4..2167d4a4c6 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -90,6 +90,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return GetString("ArgumentCannotBeNullOrEmpty"); } + /// + /// Unsupported handler method type '{0}'. + /// + internal static string UnsupportedHandlerMethodType + { + get { return GetString("UnsupportedHandlerMethodType"); } + } + + /// + /// Unsupported handler method type '{0}'. + /// + internal static string FormatUnsupportedHandlerMethodType(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedHandlerMethodType"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index b3c021e3da..406a421a44 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -132,4 +132,7 @@ Value cannot be null or empty. + + Unsupported handler method return type '{0}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json index dad336aea2..315d25a5eb 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json @@ -33,6 +33,10 @@ "version": "1.2.0-*", "type": "build" }, + "Microsoft.Extensions.ClosedGenericMatcher.Sources": { + "version": "1.2.0-*", + "type": "build" + }, "NETStandard.Library": "1.6.1" }, "frameworks": { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs new file mode 100644 index 0000000000..ca1540cb63 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ExecutorFactoryTest.cs @@ -0,0 +1,454 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Test.Internal +{ + public class ExecutorFactoryTest + { + [Fact] + public async Task CreateExecutor_ForActionResultMethod_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new TestPage(), null); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ConcreteActionResult)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new TestPage(), null); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.ActionResultReturnHandlerWithParameters)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new TestPage(), null); + var actionResult = await actionResultTask; + var contentResult = Assert.IsType(actionResult); + Assert.Equal("Hello 0", contentResult.Content); + } + + [Fact] + public async Task CreateExecutor_ForVoidReturningMethod_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var page = new TestPage(); + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(page, null); + var actionResult = await actionResultTask; + Assert.Null(actionResult); + Assert.True(page.SideEffects); + } + + [Fact] + public async Task CreateExecutor_ForVoidTaskReturningMethod_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var page = new TestPage(); + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.VoidTaskReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(page, null); + var actionResult = await actionResultTask; + Assert.Null(actionResult); + Assert.True(page.SideEffects); + } + + [Fact] + public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.GenericTaskHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new TestPage(), null); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod_OnPage() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + var methodInfo = typeof(TestPage).GetMethod(nameof(TestPage.TaskReturningConcreteSubtype)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new TestPage(), null); + var actionResult = await actionResultTask; + var contentResult = Assert.IsType(actionResult); + Assert.Equal("value", contentResult.Content); + } + + [Fact] + public async Task CreateExecutor_ForActionResultMethod_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ActionResultReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), new TestPageModel()); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForMethodReturningConcreteSubtypeOfIActionResult_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ConcreteActionResult)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), new TestPageModel()); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForActionResultReturningMethod_WithParameters_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.ActionResultReturnHandlerWithParameters)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), new TestPageModel()); + var actionResult = await actionResultTask; + var contentResult = Assert.IsType(actionResult); + Assert.Equal("Hello 0", contentResult.Content); + } + + [Fact] + public async Task CreateExecutor_ForVoidReturningMethod_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var model = new TestPageModel(); + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.VoidReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), model); + var actionResult = await actionResultTask; + Assert.Null(actionResult); + Assert.True(model.SideEffects); + } + + [Fact] + public async Task CreateExecutor_ForVoidTaskReturningMethod_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var model = new TestPageModel(); + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.VoidTaskReturningHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), model); + var actionResult = await actionResultTask; + Assert.Null(actionResult); + Assert.True(model.SideEffects); + } + + [Fact] + public async Task CreateExecutor_ForTaskOfIActionResultReturningMethod_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.GenericTaskHandler)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), new TestPageModel()); + var actionResult = await actionResultTask; + Assert.IsType(actionResult); + } + + [Fact] + public async Task CreateExecutor_ForTaskOfConcreteActionResultReturningMethod_OnPageModel() + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(nameof(TestPageModel.TaskReturningConcreteSubtype)); + + // Act + var executor = ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo); + + // Assert + Assert.NotNull(executor); + var actionResultTask = executor(new EmptyPage(), new TestPageModel()); + var actionResult = await actionResultTask; + var contentResult = Assert.IsType(actionResult); + Assert.Equal("value", contentResult.Content); + } + + [Theory] + [InlineData(nameof(TestPageModel.StringResult))] + [InlineData(nameof(TestPageModel.TaskOfObject))] + [InlineData(nameof(TestPageModel.ViewComponent))] + public void CreateExecutor_ThrowsIfTypeIsNotAValidReturnType(string methodName) + { + // Arrange + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PageModel).GetTypeInfo(), + }; + var methodInfo = typeof(TestPageModel).GetMethod(methodName); + + // Act & Assert + var ex = Assert.Throws(() => ExecutorFactory.CreateExecutor(actionDescriptor, methodInfo)); + Assert.Equal($"Unsupported handler method return type '{methodInfo.ReturnType}'.", ex.Message); + } + + private class TestPage : Page + { + public TestPage() + { + Binder = new MockBinder(); + } + + public bool SideEffects { get; private set; } + + public IActionResult ActionResultReturningHandler() => new EmptyResult(); + + public IActionResult ActionResultReturnHandlerWithParameters(int arg1, string arg2 = "Hello") + { + return new ContentResult + { + Content = $"{arg2} {arg1}", + }; + } + + public ViewResult ConcreteActionResult() => new ViewResult(); + + public void VoidReturningHandler() + { + SideEffects = true; + } + + public async Task VoidTaskReturningHandler() + { + await Task.Run(() => + { + SideEffects = true; + }); + } + + public Task GenericTaskHandler() => Task.FromResult(new EmptyResult()); + + public Task TaskReturningConcreteSubtype(string arg = "value") + { + return Task.FromResult(new ContentResult + { + Content = arg, + }); + } + + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class TestPageModel + { + public bool SideEffects { get; private set; } + + public IActionResult ActionResultReturningHandler() => new EmptyResult(); + + public IActionResult ActionResultReturnHandlerWithParameters(int arg1, string arg2 = "Hello") + { + return new ContentResult + { + Content = $"{arg2} {arg1}", + }; + } + + public ViewResult ConcreteActionResult() => new ViewResult(); + + public void VoidReturningHandler() + { + SideEffects = true; + } + + public async Task VoidTaskReturningHandler() + { + await Task.Run(() => + { + SideEffects = true; + }); + } + + public Task GenericTaskHandler() => Task.FromResult(new EmptyResult()); + + public Task TaskReturningConcreteSubtype(string arg = "value") + { + return Task.FromResult(new ContentResult + { + Content = arg, + }); + } + + public string StringResult() => ""; + + public Task TaskOfObject() => Task.FromResult(new object()); + + public IViewComponentResult ViewComponent() => new ViewViewComponentResult(); + } + + private class EmptyPage : Page + { + public EmptyPage() + { + Binder = new MockBinder(); + } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class MockBinder : PageArgumentBinder + { + protected override Task BindAsync(PageContext context, object value, string name, Type type) + { + var result = ModelBindingResult.Failed(); + return Task.FromResult(result); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index a9d639d390..3254226c5e 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -171,6 +171,51 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(new[] { factory2, factory1 }, entry.PageStartFactories); } + [Fact] + public void OnProvidersExecuting_CachesExecutor() + { + // Arrange + var descriptor = new PageActionDescriptor + { + RelativePath = "/Home/Path1/File.cshtml", + ViewEnginePath = "/Home/Path1/File.cshtml", + FilterDescriptors = new FilterDescriptor[0], + }; + + var loader = new Mock(); + loader.Setup(l => l.Load(It.IsAny())) + .Returns(typeof(PageWithModel)); + var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var actionDescriptorProvider = new Mock(); + actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var razorPageFactoryProvider = new Mock(); + var fileProvider = new TestFileProvider(); + var defaultRazorProject = new DefaultRazorProject(fileProvider); + + var invokerProvider = CreateInvokerProvider( + loader.Object, + actionDescriptorProvider.Object, + razorPageFactoryProvider: razorPageFactoryProvider.Object, + razorProject: defaultRazorProject); + var context = new ActionInvokerProviderContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + // Act + invokerProvider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.Result); + var actionInvoker = Assert.IsType(context.Result); + var actionDescriptor = actionInvoker.CacheEntry.ActionDescriptor; + Assert.Collection(actionDescriptor.HandlerMethods, + handlerDescriptor => + { + Assert.Equal(nameof(TestPageModel.OnGet), handlerDescriptor.Method.Name); + Assert.NotNull(handlerDescriptor.Executor); + }); + } + + [Fact] public void OnProvidersExecuting_CachesEntries() { @@ -291,7 +336,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private class PageWithModel { - public object Model { get; set; } + public TestPageModel Model { get; set; } + } + + private class TestPageModel + { + public void OnGet() + { + } } } }