diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelPartsProvider.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelPartsProvider.cs index 5fc10e4148..d120da58d2 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelPartsProvider.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelPartsProvider.cs @@ -2,9 +2,7 @@ // 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; diff --git a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelProvider.cs b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelProvider.cs index c146407cf1..e24972acac 100644 --- a/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelProvider.cs +++ b/src/Mvc/Mvc.RazorPages/src/ApplicationModels/DefaultPageApplicationModelProvider.cs @@ -2,11 +2,11 @@ // 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.Linq; using System.Reflection; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Internal; @@ -98,16 +98,23 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Now we want figure out which type is the handler type. TypeInfo handlerType; + var pageTypeAttributes = pageTypeInfo.GetCustomAttributes(inherit: true); + object[] handlerTypeAttributes; if (modelProperty.PropertyType.IsDefined(typeof(PageModelAttribute), inherit: true)) { handlerType = modelTypeInfo; + + // If a PageModel is specified, combine the attributes specified on the Page and the Model type. + // Attributes that appear earlier in the are more significant. In this case, we'll treat attributes on the model (code) + // to be more signficant than the page (code-generated). + handlerTypeAttributes = modelTypeInfo.GetCustomAttributes(inherit: true).Concat(pageTypeAttributes).ToArray(); } else { handlerType = pageTypeInfo; + handlerTypeAttributes = pageTypeInfo.GetCustomAttributes(inherit: true); } - var handlerTypeAttributes = handlerType.GetCustomAttributes(inherit: true); var pageModel = new PageApplicationModel( actionDescriptor, declaredModelType, diff --git a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs index 78aa991bd9..82f02f468b 100644 --- a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs @@ -525,6 +525,43 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { } + [Fact] + public void OnProvidersExecuting_CombinesFilters_OnPageAndPageModel() + { + // Arrange + var provider = CreateProvider(); + var typeInfo = typeof(PageWithFilters).GetTypeInfo(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var pageModel = context.PageApplicationModel; + Assert.Collection( + pageModel.Filters, + filter => Assert.IsType(filter), + filter => Assert.IsType(filter), + filter => Assert.IsType(filter), + filter => Assert.IsType(filter)); + } + + [ServiceFilter(typeof(Guid))] + private class PageWithFilters : Page + { + public PageWithFilterModel Model { get; } + + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + [TypeFilter(typeof(string))] + private class PageWithFilterModel : PageModel + { + } + + [ServiceFilter(typeof(IServiceProvider))] + private class FiltersOnPageAndPageModel : PageModel { } + [Fact] // If the model has handler methods, we prefer those. public void CreateDescriptor_FindsHandlerMethod_OnModel() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 4a11189cb2..2435142f2f 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -695,6 +695,40 @@ Hello from /Pages/Shared/"; await response.AssertStatusCodeAsync(HttpStatusCode.OK); } + [Fact] + public async Task AuthAttribute_AppliedOnPageWorks() + { + // Act + using var response = await Client.GetAsync("/Filters/AuthFilterOnPage"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/Login?ReturnUrl=%2FFilters%2FAuthFilterOnPage", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task AuthAttribute_AppliedOnPageWithModelWorks() + { + // Act + using var response = await Client.GetAsync("/Filters/AuthFilterOnPageWithModel"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/Login?ReturnUrl=%2FFilters%2FAuthFilterOnPageWithModel", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task FiltersAppliedToPageAndPageModelAreExecuted() + { + // Act + using var response = await Client.GetAsync("/Filters/FiltersAppliedToPageAndPageModel"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal(new[] { "PageModelFilterValue" }, response.Headers.GetValues("PageModelFilterKey")); + Assert.Equal(new[] { "PageFilterValue" }, response.Headers.GetValues("PageFilterKey")); + } + private async Task AddAntiforgeryHeadersAsync(HttpRequestMessage request) { var response = await Client.GetAsync(request.RequestUri); diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPage.cshtml b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPage.cshtml new file mode 100644 index 0000000000..54a47fbf91 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPage.cshtml @@ -0,0 +1,3 @@ +@page + +@attribute [Authorize] \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cs new file mode 100644 index 0000000000..3beb6bd6f8 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cs @@ -0,0 +1,14 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite.Pages.Filters +{ + [SkipStatusCodePages] + public class AuthFilterOnPageWithModel : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cshtml b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cshtml new file mode 100644 index 0000000000..b3df49efcf --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/AuthFilterOnPageWithModel.cshtml @@ -0,0 +1,3 @@ +@page +@model AuthFilterOnPageWithModel +@attribute [Authorize] \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cs new file mode 100644 index 0000000000..fa47e824b4 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cs @@ -0,0 +1,14 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite.Pages.Filters +{ + [TestPageModelFilter] + public class FiltersAppliedToPageAndPageModel : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cshtml b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cshtml new file mode 100644 index 0000000000..8118b1da2b --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/FiltersAppliedToPageAndPageModel.cshtml @@ -0,0 +1,3 @@ +@page +@model FiltersAppliedToPageAndPageModel +@attribute [TestPageFilter] \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageFilter.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageFilter.cs new file mode 100644 index 0000000000..98e4f633ab --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageFilter.cs @@ -0,0 +1,24 @@ +// 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.Filters; + +namespace RazorPagesWebSite.Pages.Filters +{ + public class TestPageFilter : Attribute, IPageFilter + { + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + context.HttpContext.Response.Headers["PageFilterKey"] = "PageFilterValue"; + } + + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + } + } +} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageModelFilter.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageModelFilter.cs new file mode 100644 index 0000000000..3972219edd --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/TestPageModelFilter.cs @@ -0,0 +1,20 @@ +// 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.Filters; + +namespace RazorPagesWebSite.Pages.Filters +{ + public class TestPageModelFilter : Attribute, IResourceFilter + { + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + + public void OnResourceExecuting(ResourceExecutingContext context) + { + context.HttpContext.Response.Headers["PageModelFilterKey"] = "PageModelFilterValue"; + } + } +} diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/_ViewImports.cshtml b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/_ViewImports.cshtml new file mode 100644 index 0000000000..991ca2cd2a --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Pages/Filters/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Authorization \ No newline at end of file