From 197ef139d6928d17a5c7dbfa3e4f79edb0ec7062 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 21 Sep 2017 12:33:14 -0700 Subject: [PATCH] Provide a way within the Page/PageModel to run code before any handler runs Fixes #6606 --- .../Internal/ControllerActionFilter.cs | 30 +-- .../Internal/ControllerResultFilter.cs | 30 +-- .../Filters/IPageFilter.cs | 2 +- .../DefaultPageApplicationModelProvider.cs | 14 ++ .../Internal/PageHandlerPageFilter.cs | 82 ++++++++ .../Internal/PageHandlerResultFIlter.cs | 63 ++++++ .../PageModel.cs | 76 +++++++- .../RazorPagesTest.cs | 23 +++ ...izationPageApplicationModelProviderTest.cs | 14 +- ...DefaultPageApplicationModelProviderTest.cs | 111 +++++++++++ .../Internal/PageHandlerPageFilterTest.cs | 181 ++++++++++++++++++ .../Internal/PageHandlerResultFilterTest.cs | 176 +++++++++++++++++ ...CacheFilterApplicationModelProviderTest.cs | 6 +- .../PageModelTest.cs | 94 +++++++++ .../RazorPagesWebSite/ModelAsFilter.cs | 39 ++++ .../RazorPagesWebSite/ModelAsFilter.cshtml | 3 + 16 files changed, 912 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerPageFilter.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerResultFIlter.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerPageFilterTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerResultFilterTest.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelAsFilter.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelAsFilter.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionFilter.cs index 7192eb19d6..93e4c4774a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionFilter.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal public int Order { get; set; } = int.MinValue; /// - public async Task OnActionExecutionAsync( + public Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { @@ -40,23 +40,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal nameof(ActionExecutingContext))); } - IAsyncActionFilter asyncActionFilter; - IActionFilter actionFilter; - if ((asyncActionFilter = controller as IAsyncActionFilter) != null) + if (controller is IAsyncActionFilter asyncActionFilter) { - await asyncActionFilter.OnActionExecutionAsync(context, next); + return asyncActionFilter.OnActionExecutionAsync(context, next); } - else if ((actionFilter = controller as IActionFilter) != null) + else if (controller is IActionFilter actionFilter) { - actionFilter.OnActionExecuting(context); - if (context.Result == null) - { - actionFilter.OnActionExecuted(await next()); - } + return ExecuteActionFilter(context, next, actionFilter); } else { - await next(); + return next(); + } + } + + private static async Task ExecuteActionFilter( + ActionExecutingContext context, + ActionExecutionDelegate next, + IActionFilter actionFilter) + { + actionFilter.OnActionExecuting(context); + if (context.Result == null) + { + actionFilter.OnActionExecuted(await next()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerResultFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerResultFilter.cs index 8b37ddccc7..3e30debccc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerResultFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerResultFilter.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal public int Order { get; set; } = int.MinValue; /// - public async Task OnResultExecutionAsync( + public Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) { @@ -40,23 +40,29 @@ namespace Microsoft.AspNetCore.Mvc.Internal nameof(ResultExecutingContext))); } - IAsyncResultFilter asyncResultFilter; - IResultFilter resultFilter; - if ((asyncResultFilter = controller as IAsyncResultFilter) != null) + if (controller is IAsyncResultFilter asyncResultFilter) { - await asyncResultFilter.OnResultExecutionAsync(context, next); + return asyncResultFilter.OnResultExecutionAsync(context, next); } - else if ((resultFilter = controller as IResultFilter) != null) + else if (controller is IResultFilter resultFilter) { - resultFilter.OnResultExecuting(context); - if (!context.Cancel) - { - resultFilter.OnResultExecuted(await next()); - } + return ExecuteResultFilter(context, next, resultFilter); } else { - await next(); + return next(); + } + } + + private static async Task ExecuteResultFilter( + ResultExecutingContext context, + ResultExecutionDelegate next, + IResultFilter resultFilter) + { + resultFilter.OnResultExecuting(context); + if (!context.Cancel) + { + resultFilter.OnResultExecuted(await next()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs index 8c9b45ea58..101317c94f 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters void OnPageHandlerExecuting(PageHandlerExecutingContext context); /// - /// Called after the handler method executes, before the action result. + /// Called after the handler method executes, before the action method is invoked. /// /// The . void OnPageHandlerExecuted(PageHandlerExecutedContext context); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs index 1dae7a10da..de7e25ddb5 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public class DefaultPageApplicationModelProvider : IPageApplicationModelProvider { private const string ModelPropertyName = "Model"; + private readonly PageHandlerPageFilter _pageHandlerPageFilter = new PageHandlerPageFilter(); + private readonly PageHandlerResultFilter _pageHandlerResultFilter = new PageHandlerResultFilter(); /// public int Order => -1000; @@ -142,6 +144,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal pageModel.Filters.Add(filter); } } + + if (typeof(IAsyncPageFilter).IsAssignableFrom(pageModel.HandlerType) || + typeof(IPageFilter).IsAssignableFrom(pageModel.HandlerType)) + { + pageModel.Filters.Add(_pageHandlerPageFilter); + } + + if (typeof(IAsyncResultFilter).IsAssignableFrom(pageModel.HandlerType) || + typeof(IResultFilter).IsAssignableFrom(pageModel.HandlerType)) + { + pageModel.Filters.Add(_pageHandlerResultFilter); + } } /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerPageFilter.cs new file mode 100644 index 0000000000..662fa6113a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerPageFilter.cs @@ -0,0 +1,82 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageHandlerPageFilter : IAsyncPageFilter, IOrderedFilter + { + /// + /// Filters on handlers run furthest from the action. + /// t + public int Order => int.MinValue; + + public Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + var handlerInstance = context.HandlerInstance; + if (handlerInstance == null) + { + throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(context.HandlerInstance), + nameof(PageHandlerExecutedContext))); + } + + if (handlerInstance is IAsyncPageFilter asyncPageFilter) + { + return asyncPageFilter.OnPageHandlerExecutionAsync(context, next); + } + else if (handlerInstance is IPageFilter pageFilter) + { + return ExecuteSyncFilter(context, next, pageFilter); + } + else + { + return next(); + } + } + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.HandlerInstance is IAsyncPageFilter asyncPageFilter) + { + return asyncPageFilter.OnPageHandlerSelectionAsync(context); + } + else if (context.HandlerInstance is IPageFilter pageFilter) + { + pageFilter.OnPageHandlerSelected(context); + } + + return Task.CompletedTask; + } + + private static async Task ExecuteSyncFilter( + PageHandlerExecutingContext context, + PageHandlerExecutionDelegate next, + IPageFilter pageFilter) + { + pageFilter.OnPageHandlerExecuting(context); + if (context.Result == null) + { + pageFilter.OnPageHandlerExecuted(await next()); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerResultFIlter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerResultFIlter.cs new file mode 100644 index 0000000000..9d0b3dcd09 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageHandlerResultFIlter.cs @@ -0,0 +1,63 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageHandlerResultFilter : IAsyncResultFilter, IOrderedFilter + { + /// + /// Filters on handlers run furthest from the action. + /// + public int Order => int.MinValue; + + public Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + var handler = context.Controller; + if (handler == null) + { + throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(context.Controller), + nameof(ResultExecutingContext))); + } + + if (handler is IAsyncResultFilter asyncResultFilter) + { + return asyncResultFilter.OnResultExecutionAsync(context, next); + } + else if (handler is IResultFilter resultFilter) + { + return ExecuteSyncFilter(context, next, resultFilter); + } + else + { + return next(); + } + } + + private static async Task ExecuteSyncFilter( + ResultExecutingContext context, + ResultExecutionDelegate next, + IResultFilter resultFilter) + { + resultFilter.OnResultExecuting(context); + if (!context.Cancel) + { + resultFilter.OnResultExecuted(await next()); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs index 91ea905afe..0114e2a94c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -21,8 +22,8 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.RazorPages { - [PageModelAttribute] - public abstract class PageModel + [PageModel] + public abstract class PageModel : IAsyncPageFilter, IPageFilter { private IModelMetadataProvider _metadataProvider; private IModelBinderFactory _modelBinderFactory; @@ -1601,5 +1602,74 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages model: model); return ModelState.IsValid; } + +#region IAsyncPageFilter \ IPageFilter + /// + /// Called after a handler method has been selected, but before model binding occurs. + /// + /// The . + public virtual void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + } + + /// + /// Called before the handler method executes, after model binding is complete. + /// + /// The . + public virtual void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + } + + /// + /// Called after the handler method executes, before the action method is invoked. + /// + /// The . + public virtual void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + + /// + /// Called asynchronously after the handler method has been selected, but before model binding occurs. + /// + /// The . + /// A that on completion indicates the filter has executed. + public virtual Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + OnPageHandlerSelected(context); + return Task.CompletedTask; + } + + /// + /// Called asynchronously before the handler method is invoked, after model binding is complete. + /// + /// The . + /// + /// The . Invoked to execute the next page filter or the handler method itself. + /// + /// A that on completion indicates the filter has executed. + public virtual async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + OnPageHandlerExecuting(context); + if (context.Result == null) + { + OnPageHandlerExecuted(await next()); + } + } +#endregion } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 4e7dc2f02a..eac69b9e36 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1209,6 +1209,29 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.Equal(expected, response.Trim()); } + [Fact] + public async Task PageHandlerFilterOnPageModelIsExecuted() + { + // Arrange + var expected = "Hello from OnPageHandlerExecuting"; + + // Act + var response = await Client.GetStringAsync("/ModelAsFilter?message=Hello+world"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + + [Fact] + public async Task ResultFilterOnPageModelIsExecuted() + { + // Act + var response = await Client.GetAsync("/ModelAsFilter/TestResultFilter"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs index feb20c2b98..3d02c51a68 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Mvc.Razor; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -27,7 +26,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal autorizationProvider.OnProvidersExecuting(context); // Assert - Assert.Empty(context.PageApplicationModel.Filters); + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f)); } private class PageWithAuthorizeHandlers : Page @@ -59,6 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( context.PageApplicationModel.Filters, + f => Assert.IsType(f), f => Assert.IsType(f)); } @@ -94,7 +96,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal autorizationProvider.OnProvidersExecuting(context); // Assert - var authorizeFilter = Assert.IsType(Assert.Single(context.PageApplicationModel.Filters)); + AuthorizeFilter authorizeFilter = null; + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f), + f => authorizeFilter = Assert.IsType(f)); + // Basic + Basic2 + Derived authorize Assert.Equal(3, authorizeFilter.Policy.Requirements.Count); } @@ -133,6 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Collection( context.PageApplicationModel.Filters, + f => Assert.IsType(f), f => Assert.IsType(f)); } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs index 3bf8b5f2ed..afc0c41b1a 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; 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; @@ -1032,5 +1033,115 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public void OnGetUser() { } } + + [Fact] + public void PopulateFilters_AddsIFilterMetadataAttributesToModel() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(FilterModel).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter)); + } + + [PageModel] + [Serializable] + [TypeFilter(typeof(object))] + private class FilterModel + { + } + + [Fact] + public void PopulateFilters_AddsPageHandlerPageFilter_IfPageImplementsIAsyncPageFilter() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(ModelImplementingAsyncPageFilter).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter)); + } + + private class ModelImplementingAsyncPageFilter : IAsyncPageFilter + { + public Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + throw new NotImplementedException(); + } + + public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateFilters_AddsPageHandlerPageFilter_IfPageImplementsIPageFilter() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(ModelImplementingPageFilter).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter)); + } + + private class ModelImplementingPageFilter : IPageFilter + { + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + throw new NotImplementedException(); + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + throw new NotImplementedException(); + } + + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateFilters_AddsPageHandlerPageFilter_ForModelDerivingFromTypeImplementingPageFilter() + { + // Arrange + var provider = new DefaultPageApplicationModelProvider(); + var typeInfo = typeof(DerivedFromPageModel).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, typeInfo.GetCustomAttributes(inherit: true)); + + // Act + provider.PopulateFilters(pageModel); + + // Assert + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); + } + + [ServiceFilter(typeof(IServiceProvider))] + private class DerivedFromPageModel : PageModel { } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerPageFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerPageFilterTest.cs new file mode 100644 index 0000000000..6b8e1e5d17 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerPageFilterTest.cs @@ -0,0 +1,181 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageHandlerPageFilterTest + { + [Fact] + public async Task OnPageHandlerExecutionAsync_ExecutesAsyncFilters() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + model.Object); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + model.Object); + PageHandlerExecutionDelegate next = () => Task.FromResult(pageHandlerExecutedContext); + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnPageHandlerExecutionAsync(pageHandlerExecutingContext, next)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var pageHandlerPageFilter = new PageHandlerPageFilter(); + + // Act + await pageHandlerPageFilter.OnPageHandlerExecutionAsync(pageHandlerExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnPageHandlerExecutionAsync_ExecutesSyncFilters() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Verifiable(); + + modelAsFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Verifiable(); + + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + model.Object); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + model.Object); + PageHandlerExecutionDelegate next = () => Task.FromResult(pageHandlerExecutedContext); + + var pageHandlerPageFilter = new PageHandlerPageFilter(); + + // Act + await pageHandlerPageFilter.OnPageHandlerExecutionAsync(pageHandlerExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnPageHandlerExecutionAsync_DoesNotInvokeHandlerExecuted_IfResultIsSet() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback((PageHandlerExecutingContext context) => context.Result = new PageResult()) + .Verifiable(); + + modelAsFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Throws(new Exception("Shouldn't be called")); + + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + model.Object); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + model.Object); + PageHandlerExecutionDelegate next = () => Task.FromResult(pageHandlerExecutedContext); + + var pageHandlerPageFilter = new PageHandlerPageFilter(); + + // Act + await pageHandlerPageFilter.OnPageHandlerExecutionAsync(pageHandlerExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnPageHandlerExecutionAsync_InvokesNextDelegateIfHandlerDoesNotImplementFilter() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new object(); + + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + model); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + model); + var invoked = false; + PageHandlerExecutionDelegate next = () => + { + invoked = true; + return Task.FromResult(pageHandlerExecutedContext); + }; + + var pageHandlerPageFilter = new PageHandlerPageFilter(); + + // Act + await pageHandlerPageFilter.OnPageHandlerExecutionAsync(pageHandlerExecutingContext, next); + + // Assert + Assert.True(invoked); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerResultFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerResultFilterTest.cs new file mode 100644 index 0000000000..46b7996080 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageHandlerResultFilterTest.cs @@ -0,0 +1,176 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageHandlerResultFilterTest + { + [Fact] + public async Task OnResultExecutionAsync_ExecutesAsyncFilters() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var resultExecutingContext = new ResultExecutingContext( + pageContext, + Array.Empty(), + new PageResult(), + model.Object); + var resultExecutedContext = new ResultExecutedContext( + pageContext, + Array.Empty(), + resultExecutingContext.Result, + model.Object); + ResultExecutionDelegate next = () => Task.FromResult(resultExecutedContext); + + var pageHandlerResultFilter = new PageHandlerResultFilter(); + + // Act + await pageHandlerResultFilter.OnResultExecutionAsync(resultExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnResultExecutionAsyn_ExecutesSyncFilters() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Verifiable(); + + modelAsFilter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Verifiable(); + + var resultExecutingContext = new ResultExecutingContext( + pageContext, + Array.Empty(), + new PageResult(), + model.Object); + var resultExecutedContext = new ResultExecutedContext( + pageContext, + Array.Empty(), + resultExecutingContext.Result, + model.Object); + ResultExecutionDelegate next = () => Task.FromResult(resultExecutedContext); + + var pageHandlerResultFilter = new PageHandlerResultFilter(); + + // Act + await pageHandlerResultFilter.OnResultExecutionAsync(resultExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnPageHandlerExecutionAsync_DoesNotInvokeResultExecuted_IfCancelled() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new Mock(); + + var modelAsFilter = model.As(); + modelAsFilter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback((ResultExecutingContext context) => context.Cancel = true) + .Verifiable(); + + modelAsFilter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Throws(new Exception("Shouldn't be called")); + + var resultExecutingContext = new ResultExecutingContext( + pageContext, + Array.Empty(), + new PageResult(), + model.Object); + var resultExecutedContext = new ResultExecutedContext( + pageContext, + Array.Empty(), + resultExecutingContext.Result, + model.Object); + ResultExecutionDelegate next = () => Task.FromResult(resultExecutedContext); + + var pageHandlerResultFilter = new PageHandlerResultFilter(); + + // Act + await pageHandlerResultFilter.OnResultExecutionAsync(resultExecutingContext, next); + + // Assert + modelAsFilter.Verify(); + } + + [Fact] + public async Task OnPageHandlerExecutionAsync_InvokesNextDelegateIfHandlerDoesNotImplementFilter() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var model = new object(); + + var resultExecutingContext = new ResultExecutingContext( + pageContext, + Array.Empty(), + new PageResult(), + model); + var resultExecutedContext = new ResultExecutedContext( + pageContext, + Array.Empty(), + resultExecutingContext.Result, + model); + var invoked = false; + ResultExecutionDelegate next = () => + { + invoked = true; + return Task.FromResult(resultExecutedContext); + }; + + var pageHandlerResultFilter = new PageHandlerResultFilter(); + + // Act + await pageHandlerResultFilter.OnResultExecutionAsync(resultExecutingContext, next); + + // Assert + Assert.True(invoked); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs index 0bea6bd5cc..e532ca26c1 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs @@ -25,7 +25,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal provider.OnProvidersExecuting(context); // Assert - Assert.Empty(context.PageApplicationModel.Filters); + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f)); } private class PageWithoutResponseCache : Page @@ -59,6 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( context.PageApplicationModel.Filters, f => { }, + f => Assert.IsType(f), f => { var filter = Assert.IsType(f); @@ -104,6 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Collection( context.PageApplicationModel.Filters, f => { }, + f => Assert.IsType(f), f => { var filter = Assert.IsType(f); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs index f5b93119e8..fee28676ae 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/PageModelTest.cs @@ -7,6 +7,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; @@ -1762,6 +1763,99 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages Assert.Null(pageResult.Page); // This is set by the invoker } + [Fact] + public async Task AsyncPageHandlerExecutingMethod_InvokeSyncMethods() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + new object()); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new object()); + var testPageModel = new Mock { CallBase = true }; + testPageModel.Setup(p => p.OnPageHandlerExecuting(pageHandlerExecutingContext)) + .Verifiable(); + testPageModel.Setup(p => p.OnPageHandlerExecuted(pageHandlerExecutedContext)) + .Verifiable(); + + // Act + await testPageModel.Object.OnPageHandlerExecutionAsync( + pageHandlerExecutingContext, + () => Task.FromResult(pageHandlerExecutedContext)); + + testPageModel.Verify(); + } + + [Fact] + public async Task AsyncPageHandlerExecutingMethod__DoesNotInvokeExecutedMethod_IfResultIsSet() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var pageHandlerExecutingContext = new PageHandlerExecutingContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new Dictionary(), + new object()); + var pageHandlerExecutedContext = new PageHandlerExecutedContext( + pageContext, + Array.Empty(), + new HandlerMethodDescriptor(), + new object()); + var testPageModel = new Mock() { CallBase = true }; + testPageModel.Setup(p => p.OnPageHandlerExecuting(pageHandlerExecutingContext)) + .Callback((PageHandlerExecutingContext context) => context.Result = new PageResult()) + .Verifiable(); + testPageModel.Setup(p => p.OnPageHandlerExecuted(pageHandlerExecutedContext)) + .Throws(new Exception("Shouldn't be called")); + + // Act + await testPageModel.Object.OnPageHandlerExecutionAsync( + pageHandlerExecutingContext, + () => Task.FromResult(pageHandlerExecutedContext)); + + testPageModel.Verify(); + } + + [Fact] + public async Task AsyncPageHandlerSelectingMethod_InvokeSyncMethods() + { + // Arrange + var pageContext = new PageContext(new ActionContext( + new DefaultHttpContext(), + new RouteData(), + new PageActionDescriptor(), + new ModelStateDictionary())); + var pageHandlerSelectedContext = new PageHandlerSelectedContext( + pageContext, + Array.Empty(), + new object()); + + var testPageModel = new Mock { CallBase = true }; + testPageModel.Setup(p => p.OnPageHandlerSelected(pageHandlerSelectedContext)) + .Verifiable(); + + // Act + await testPageModel.Object.OnPageHandlerSelectionAsync(pageHandlerSelectedContext); + + testPageModel.Verify(); + } + private class ContentPageModel : PageModel { public IActionResult Content_WithNoEncoding() diff --git a/test/WebSites/RazorPagesWebSite/ModelAsFilter.cs b/test/WebSites/RazorPagesWebSite/ModelAsFilter.cs new file mode 100644 index 0000000000..503e9eb21c --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelAsFilter.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class ModelAsFilter : PageModel, IResultFilter + { + public string Message { get; set; } + + public void OnGet(string message) + { + Message = message; + } + + public IActionResult OnGetTestResultFilter() => NotFound(); + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + context.HandlerArguments["message"] = "Hello from OnPageHandlerExecuting"; + } + + public void OnResultExecuted(ResultExecutedContext context) + { + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is NotFoundResult) + { + context.Result = Redirect("/Different-Location"); + } + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/ModelAsFilter.cshtml b/test/WebSites/RazorPagesWebSite/ModelAsFilter.cshtml new file mode 100644 index 0000000000..61d5fce1a9 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelAsFilter.cshtml @@ -0,0 +1,3 @@ +@page "{handler?}" +@model RazorPagesWebSite.ModelAsFilter +@Model.Message