From 900a5c7c4c789e71bf18e8e90bf9b28b4e51013f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 3 Aug 2017 10:02:23 -0700 Subject: [PATCH] Add support for ResponseCache in Razor Pages Fixes #6437 --- .../Internal/IResponseCacheFilter.cs | 4 +- .../Internal/ResponseCacheFilter.cs | 125 +--- .../Internal/ResponseCacheFilterExecutor.cs | 153 +++++ .../ResponseCacheAttribute.cs | 40 +- .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 2 + .../Internal/ResponseCacheFilter.cs | 103 +++ ...onseCacheFilterApplicationModelProvider.cs | 48 ++ .../ResponseCacheFilterExecutorTest.cs | 576 +++++++++++++++++ .../Internal/ResponseCacheFilterTest.cs | 586 ------------------ .../RazorPagesTest.cs | 17 + ...CacheFilterApplicationModelProviderTest.cs | 138 +++++ .../MvcServiceCollectionExtensionsTest.cs | 1 + .../ModelWithResponseCache.cs | 21 + .../ModelWithResponseCache.cshtml | 4 + 14 files changed, 1105 insertions(+), 713 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilterExecutor.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilter.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilterApplicationModelProvider.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterExecutorTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IResponseCacheFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IResponseCacheFilter.cs index bec568b23a..5b53c254dd 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IResponseCacheFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/IResponseCacheFilter.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Microsoft.AspNetCore.Mvc.Internal { /// - /// An which sets the appropriate headers related to Response caching. + /// A filter which sets the appropriate headers related to Response caching. /// - public interface IResponseCacheFilter : IActionFilter + public interface IResponseCacheFilter : IFilterMetadata { } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs index 7d62d5644c..5138927c89 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs @@ -2,27 +2,16 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Globalization; -using System.Linq; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.ResponseCaching; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Internal { /// /// An which sets the appropriate headers related to response caching. /// - public class ResponseCacheFilter : IResponseCacheFilter + public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter { - private readonly CacheProfile _cacheProfile; - private int? _cacheDuration; - private ResponseCacheLocation? _cacheLocation; - private bool? _cacheNoStore; - private string _cacheVaryByHeader; - private string[] _cacheVaryByQueryKeys; + private readonly ResponseCacheFilterExecutor _executor; /// /// Creates a new instance of @@ -31,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// . public ResponseCacheFilter(CacheProfile cacheProfile) { - _cacheProfile = cacheProfile; + _executor = new ResponseCacheFilterExecutor(cacheProfile); } /// @@ -41,8 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public int Duration { - get { return (_cacheDuration ?? _cacheProfile.Duration) ?? 0; } - set { _cacheDuration = value; } + get => _executor.Duration; + set => _executor.Duration = value; } /// @@ -50,8 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public ResponseCacheLocation Location { - get { return (_cacheLocation ?? _cacheProfile.Location) ?? ResponseCacheLocation.Any; } - set { _cacheLocation = value; } + get => _executor.Location; + set => _executor.Location = value; } /// @@ -62,8 +51,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public bool NoStore { - get { return (_cacheNoStore ?? _cacheProfile.NoStore) ?? false; } - set { _cacheNoStore = value; } + get => _executor.NoStore; + set => _executor.NoStore = value; } /// @@ -71,8 +60,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public string VaryByHeader { - get { return _cacheVaryByHeader ?? _cacheProfile.VaryByHeader; } - set { _cacheVaryByHeader = value; } + get => _executor.VaryByHeader; + set => _executor.VaryByHeader = value; } /// @@ -83,8 +72,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public string[] VaryByQueryKeys { - get { return _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys; } - set { _cacheVaryByQueryKeys = value; } + get => _executor.VaryByQueryKeys; + set => _executor.VaryByQueryKeys = value; } /// @@ -97,101 +86,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal // If there are more filters which can override the values written by this filter, // then skip execution of this filter. - if (IsOverridden(context)) + if (ResponseCacheFilterExecutor.IsOverridden(this, context)) { return; } - if (!NoStore) - { - // Duration MUST be set (either in the cache profile or in this filter) unless NoStore is true. - if (_cacheProfile.Duration == null && _cacheDuration == null) - { - throw new InvalidOperationException( - Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration))); - } - } - - var headers = context.HttpContext.Response.Headers; - - // Clear all headers - headers.Remove(HeaderNames.Vary); - headers.Remove(HeaderNames.CacheControl); - headers.Remove(HeaderNames.Pragma); - - if (!string.IsNullOrEmpty(VaryByHeader)) - { - headers[HeaderNames.Vary] = VaryByHeader; - } - - if (VaryByQueryKeys != null) - { - var responseCachingFeature = context.HttpContext.Features.Get(); - if (responseCachingFeature == null) - { - throw new InvalidOperationException(Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys))); - } - responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys; - } - - if (NoStore) - { - headers[HeaderNames.CacheControl] = "no-store"; - - // Cache-control: no-store, no-cache is valid. - if (Location == ResponseCacheLocation.None) - { - headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache"); - headers[HeaderNames.Pragma] = "no-cache"; - } - } - else - { - string cacheControlValue = null; - switch (Location) - { - case ResponseCacheLocation.Any: - cacheControlValue = "public"; - break; - case ResponseCacheLocation.Client: - cacheControlValue = "private"; - break; - case ResponseCacheLocation.None: - cacheControlValue = "no-cache"; - headers[HeaderNames.Pragma] = "no-cache"; - break; - } - - cacheControlValue = string.Format( - CultureInfo.InvariantCulture, - "{0}{1}max-age={2}", - cacheControlValue, - cacheControlValue != null ? "," : null, - Duration); - - if (cacheControlValue != null) - { - headers[HeaderNames.CacheControl] = cacheControlValue; - } - } + _executor.Execute(context); } /// public void OnActionExecuted(ActionExecutedContext context) { } - - // internal for Unit Testing purposes. - internal bool IsOverridden(ActionExecutingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Return true if there are any filters which are after the current filter. In which case the current - // filter should be skipped. - return context.Filters.OfType().Last() != this; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilterExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilterExecutor.cs new file mode 100644 index 0000000000..ee86c5d860 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilterExecutor.cs @@ -0,0 +1,153 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.ResponseCaching; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ResponseCacheFilterExecutor + { + private readonly CacheProfile _cacheProfile; + private int? _cacheDuration; + private ResponseCacheLocation? _cacheLocation; + private bool? _cacheNoStore; + private string _cacheVaryByHeader; + private string[] _cacheVaryByQueryKeys; + + public ResponseCacheFilterExecutor(CacheProfile cacheProfile) + { + _cacheProfile = cacheProfile ?? throw new ArgumentNullException(nameof(cacheProfile)); + } + + public int Duration + { + get => _cacheDuration ?? _cacheProfile.Duration ?? 0; + set => _cacheDuration = value; + } + + public ResponseCacheLocation Location + { + get => _cacheLocation ?? _cacheProfile.Location ?? ResponseCacheLocation.Any; + set => _cacheLocation = value; + } + + public bool NoStore + { + get => _cacheNoStore ?? _cacheProfile.NoStore ?? false; + set => _cacheNoStore = value; + } + + public string VaryByHeader + { + get => _cacheVaryByHeader ?? _cacheProfile.VaryByHeader; + set => _cacheVaryByHeader = value; + } + + public string[] VaryByQueryKeys + { + get => _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys; + set => _cacheVaryByQueryKeys = value; + } + + public void Execute(FilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!NoStore) + { + // Duration MUST be set (either in the cache profile or in this filter) unless NoStore is true. + if (_cacheProfile.Duration == null && _cacheDuration == null) + { + throw new InvalidOperationException( + Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration))); + } + } + + var headers = context.HttpContext.Response.Headers; + + // Clear all headers + headers.Remove(HeaderNames.Vary); + headers.Remove(HeaderNames.CacheControl); + headers.Remove(HeaderNames.Pragma); + + if (!string.IsNullOrEmpty(VaryByHeader)) + { + headers[HeaderNames.Vary] = VaryByHeader; + } + + if (VaryByQueryKeys != null) + { + var responseCachingFeature = context.HttpContext.Features.Get(); + if (responseCachingFeature == null) + { + throw new InvalidOperationException( + Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys))); + } + responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys; + } + + if (NoStore) + { + headers[HeaderNames.CacheControl] = "no-store"; + + // Cache-control: no-store, no-cache is valid. + if (Location == ResponseCacheLocation.None) + { + headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache"); + headers[HeaderNames.Pragma] = "no-cache"; + } + } + else + { + string cacheControlValue; + switch (Location) + { + case ResponseCacheLocation.Any: + cacheControlValue = "public,"; + break; + case ResponseCacheLocation.Client: + cacheControlValue = "private,"; + break; + case ResponseCacheLocation.None: + cacheControlValue = "no-cache,"; + headers[HeaderNames.Pragma] = "no-cache"; + break; + default: + cacheControlValue = null; + break; + } + + cacheControlValue = $"{cacheControlValue}max-age={Duration}"; + headers[HeaderNames.CacheControl] = cacheControlValue; + } + } + + public static bool IsOverridden(IResponseCacheFilter executingFilter, FilterContext context) + { + Debug.Assert(context != null); + + // Return true if there are any filters which are after the current filter. In which case the current + // filter should be skipped. + for (var i = context.Filters.Count - 1; i >= 0; i--) + { + var filter = context.Filters[i]; + if (filter is IResponseCacheFilter) + { + return !object.ReferenceEquals(executingFilter, filter); + } + } + + Debug.Fail("The executing filter must be part of the filter context."); + return false; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs index df6a06b9ae..e72747a326 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs @@ -78,20 +78,16 @@ namespace Microsoft.AspNetCore.Mvc /// public bool IsReusable => true; - /// - public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + /// + /// Gets the for this attribute. + /// + /// + public CacheProfile GetCacheProfile(MvcOptions options) { - if (serviceProvider == null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - var optionsAccessor = serviceProvider.GetRequiredService>(); - CacheProfile selectedProfile = null; if (CacheProfileName != null) { - optionsAccessor.Value.CacheProfiles.TryGetValue(CacheProfileName, out selectedProfile); + options.CacheProfiles.TryGetValue(CacheProfileName, out selectedProfile); if (selectedProfile == null) { throw new InvalidOperationException(Resources.FormatCacheProfileNotFound(CacheProfileName)); @@ -109,16 +105,30 @@ namespace Microsoft.AspNetCore.Mvc VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader; VaryByQueryKeys = VaryByQueryKeys ?? selectedProfile?.VaryByQueryKeys; - // ResponseCacheFilter cannot take any null values. Hence, if there are any null values, - // the properties convert them to their defaults and are passed on. - return new ResponseCacheFilter(new CacheProfile + return new CacheProfile { Duration = _duration, Location = _location, NoStore = _noStore, VaryByHeader = VaryByHeader, VaryByQueryKeys = VaryByQueryKeys, - }); + }; + } + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + var optionsAccessor = serviceProvider.GetRequiredService>(); + var cacheProfile = GetCacheProfile(optionsAccessor.Value); + + // ResponseCacheFilter cannot take any null values. Hence, if there are any null values, + // the properties convert them to their defaults and are passed on. + return new ResponseCacheFilter(cacheProfile); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 605dd6d9d5..f4441b8388 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -99,6 +99,8 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilter.cs new file mode 100644 index 0000000000..353493550a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilter.cs @@ -0,0 +1,103 @@ +// 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; +using Microsoft.AspNetCore.Mvc.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + /// + /// A which sets the appropriate headers related to response caching. + /// + public class ResponseCacheFilter : IPageFilter, IResponseCacheFilter + { + private readonly ResponseCacheFilterExecutor _executor; + + /// + /// Creates a new instance of + /// + /// The profile which contains the settings for + /// . + public ResponseCacheFilter(CacheProfile cacheProfile) + { + _executor = new ResponseCacheFilterExecutor(cacheProfile); + } + + /// + /// Gets or sets the duration in seconds for which the response is cached. + /// This is a required parameter. + /// This sets "max-age" in "Cache-control" header. + /// + public int Duration + { + get => _executor.Duration; + set => _executor.Duration = value; + } + + /// + /// Gets or sets the location where the data from a particular URL must be cached. + /// + public ResponseCacheLocation Location + { + get => _executor.Location; + set => _executor.Location = value; + } + + /// + /// Gets or sets the value which determines whether the data should be stored or not. + /// When set to , it sets "Cache-control" header to "no-store". + /// Ignores the "Location" parameter for values other than "None". + /// Ignores the "duration" parameter. + /// + public bool NoStore + { + get => _executor.NoStore; + set => _executor.NoStore = value; + } + + /// + /// Gets or sets the value for the Vary response header. + /// + public string VaryByHeader + { + get => _executor.VaryByHeader; + set => _executor.VaryByHeader = value; + } + + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the response cache middleware. + /// + public string[] VaryByQueryKeys + { + get => _executor.VaryByQueryKeys; + set => _executor.VaryByQueryKeys = value; + } + + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (ResponseCacheFilterExecutor.IsOverridden(this, context)) + { + return; + } + + _executor.Execute(context); + } + + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilterApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilterApplicationModelProvider.cs new file mode 100644 index 0000000000..5abfc77f1e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ResponseCacheFilterApplicationModelProvider.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class ResponseCacheFilterApplicationModelProvider : IPageApplicationModelProvider + { + private readonly MvcOptions _mvcOptions; + + public ResponseCacheFilterApplicationModelProvider(IOptions mvcOptionsAccessor) + { + if (mvcOptionsAccessor == null) + { + throw new ArgumentNullException(nameof(mvcOptionsAccessor)); + } + + _mvcOptions = mvcOptionsAccessor.Value; + } + + // The order is set to execute after the DefaultPageApplicationModelProvider. + public int Order => -1000 + 10; + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var pageModel = context.PageApplicationModel; + var responseCacheAttributes = pageModel.HandlerTypeAttributes.OfType(); + foreach (var attribute in responseCacheAttributes) + { + var cacheProfile = attribute.GetCacheProfile(_mvcOptions); + context.PageApplicationModel.Filters.Add(new ResponseCacheFilter(cacheProfile)); + } + } + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterExecutorTest.cs new file mode 100644 index 0000000000..23d71ae5f3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterExecutorTest.cs @@ -0,0 +1,576 @@ +// 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.ResponseCaching; +using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ResponseCacheFilterExecutorTest + { + [Fact] + public void Execute_DoesNotThrow_WhenNoStoreIsTrue() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + NoStore = true, + Duration = null + }); + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("no-store", context.HttpContext.Response.Headers["Cache-control"]); + } + + [Fact] + public void Execute_DoesNotThrowIfDurationIsNotSet_WhenNoStoreIsFalse() + { + // Arrange, Act + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + Duration = null + }); + + // Assert + Assert.NotNull(executor); + } + + [Fact] + public void Execute_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile() + { + Duration = null + }); + + var context = GetActionExecutingContext(); + + // Act & Assert + var ex = Assert.Throws(() => executor.Execute(context)); + Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.", + ex.Message); + } + + public static TheoryData CacheControlData + { + get + { + return new TheoryData + { + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + // If no-store is set, then location is ignored. + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Client, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + // If no-store is set, then duration is ignored. + { + new CacheProfile + { + Duration = 100, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = null + }, + "private,max-age=10" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null + }, + "public,max-age=10" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.None, + NoStore = false, + VaryByHeader = null + }, + "no-cache,max-age=10" + }, + { + new CacheProfile + { + Duration = 31536000, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null + }, + "public,max-age=31536000" + }, + { + new CacheProfile + { + Duration = 20, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null + }, + "public,max-age=20" + } + }; + } + } + + [Theory] + [MemberData(nameof(CacheControlData))] + public void Execute_CanSetCacheControlHeaders(CacheProfile cacheProfile, string output) + { + // Arrange + var executor = new ResponseCacheFilterExecutor(cacheProfile); + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); + } + + public static TheoryData NoStoreData + { + get + { + return new TheoryData + { + // If no-store is set, then location is ignored. + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Client, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }, + "no-store" + }, + // If no-store is set, then duration is ignored. + { + new CacheProfile + { + Duration = 100, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }, + "no-store" + } + }; + } + } + + [Theory] + [MemberData(nameof(NoStoreData))] + public void Execute_DoesNotSetLocationOrDuration_IfNoStoreIsSet(CacheProfile cacheProfile, string output) + { + // Arrange + var executor = new ResponseCacheFilterExecutor(cacheProfile); + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); + } + + public static TheoryData VaryByHeaderData + { + get + { + return new TheoryData + { + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = "Accept" + }, + "Accept", + "public,max-age=10" + }, + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = "Accept" + }, + "Accept", + "no-store" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = "Accept" + }, + "Accept", + "private,max-age=10" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = "Test" + }, + "Test", + "private,max-age=10" + }, + { + new CacheProfile + { + Duration = 31536000, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = "Test" + }, + "Test", + "public,max-age=31536000" + } + }; + } + } + + [Theory] + [MemberData(nameof(VaryByHeaderData))] + public void ResponseCacheCanSetVaryByHeader(CacheProfile cacheProfile, string varyOutput, string cacheControlOutput) + { + // Arrange + var executor = new ResponseCacheFilterExecutor(cacheProfile); + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal(varyOutput, context.HttpContext.Response.Headers["Vary"]); + Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]); + } + + public static TheoryData VaryByQueryKeyData + { + get + { + return new TheoryData + { + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByQueryKeys = new[] { "Accept" } + }, + new[] { "Accept" }, + "public,max-age=10" + }, + { + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByQueryKeys = new[] { "Accept" } + }, + new[] { "Accept" }, + "no-store" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByQueryKeys = new[] { "Accept" } + }, + new[] { "Accept" }, + "private,max-age=10" + }, + { + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByQueryKeys = new[] { "Accept", "Test" } + }, + new[] { "Accept", "Test" }, + "private,max-age=10" + }, + { + + new CacheProfile + { + Duration = 31536000, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByQueryKeys = new[] { "Accept", "Test" } + }, + new[] { "Accept", "Test" }, + "public,max-age=31536000" + } + }; + } + } + + [Theory] + [MemberData(nameof(VaryByQueryKeyData))] + public void ResponseCacheCanSetVaryByQueryKeys(CacheProfile cacheProfile, string[] varyOutput, string cacheControlOutput) + { + // Arrange + var executor = new ResponseCacheFilterExecutor(cacheProfile); + var context = GetActionExecutingContext(); + context.HttpContext.Features.Set(new ResponseCachingFeature()); + + // Acts + executor.Execute(context); + + // Assert + Assert.Equal(varyOutput, context.HttpContext.Features.Get().VaryByQueryKeys); + Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers[HeaderNames.CacheControl]); + } + + [Fact] + public void NonEmptyVaryByQueryKeys_WithoutConfiguringMiddleware_Throws() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.None, + NoStore = true, + VaryByHeader = null, + VaryByQueryKeys = new[] { "Test" } + }); + var context = GetActionExecutingContext(); + + // Act & Assert + var exception = Assert.Throws(() => executor.Execute(context)); + Assert.Equal("'VaryByQueryKeys' requires the response cache middleware.", exception.Message); + } + + [Fact] + public void SetsPragmaOnNoCache() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.None, + NoStore = true, + VaryByHeader = null + }); + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("no-store,no-cache", context.HttpContext.Response.Headers["Cache-control"]); + Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]); + } + + [Fact] + public void IsOverridden_ReturnsTrueForAllButLastFilter() + { + // Arrange + var filter1 = new ResponseCacheFilter(new CacheProfile()); + var filter2 = new ResponseCacheFilter(new CacheProfile()); + var filters = new List + { + filter1, + Mock.Of(), + filter2, + Mock.Of(), + }; + var context = GetActionExecutingContext(filters); + + // Act & Assert + Assert.True(ResponseCacheFilterExecutor.IsOverridden(filter1, context)); + Assert.False(ResponseCacheFilterExecutor.IsOverridden(filter2, context)); + } + + [Fact] + public void IsOverridden_ReturnsTrueIfInstanceIsTheOnlyResponseCacheFilter() + { + // Arrange + var filter = new ResponseCacheFilter(new CacheProfile()); + var filters = new List + { + Mock.Of(), + filter, + Mock.Of(), + Mock.Of(), + }; + var context = GetActionExecutingContext(filters); + + // Act & Assert + Assert.False(ResponseCacheFilterExecutor.IsOverridden(filter, context)); + } + + [Fact] + public void FilterDurationProperty_OverridesCachePolicySetting() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + Duration = 10 + }); + executor.Duration = 20; + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("public,max-age=20", context.HttpContext.Response.Headers["Cache-control"]); + } + + [Fact] + public void FilterLocationProperty_OverridesCachePolicySetting() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.None + }); + executor.Location = ResponseCacheLocation.Client; + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("private,max-age=10", context.HttpContext.Response.Headers["Cache-control"]); + } + + [Fact] + public void FilterNoStoreProperty_OverridesCachePolicySetting() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + NoStore = true + }); + executor.NoStore = false; + executor.Duration = 10; + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("public,max-age=10", context.HttpContext.Response.Headers["Cache-control"]); + } + + [Fact] + public void FilterVaryByProperty_OverridesCachePolicySetting() + { + // Arrange + var executor = new ResponseCacheFilterExecutor( + new CacheProfile + { + NoStore = true, + VaryByHeader = "Accept" + }); + executor.VaryByHeader = "Test"; + var context = GetActionExecutingContext(); + + // Act + executor.Execute(context); + + // Assert + Assert.Equal("Test", context.HttpContext.Response.Headers["Vary"]); + } + + private ActionExecutingContext GetActionExecutingContext(List filters = null) + { + return new ActionExecutingContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), + filters ?? new List(), + new Dictionary(), + new object()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs deleted file mode 100644 index d87bc5aba0..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs +++ /dev/null @@ -1,586 +0,0 @@ -// 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 Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.ResponseCaching; -using Microsoft.AspNetCore.Routing; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class ResponseCacheFilterTest - { - [Fact] - public void ResponseCacheFilter_DoesNotThrow_WhenNoStoreIsTrue() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - NoStore = true, - Duration = null - }); - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("no-store", context.HttpContext.Response.Headers["Cache-control"]); - } - - [Fact] - public void ResponseCacheFilter_DoesNotThrowIfDurationIsNotSet_WhenNoStoreIsFalse() - { - // Arrange, Act - var cache = new ResponseCacheFilter( - new CacheProfile - { - Duration = null - }); - - // Assert - Assert.NotNull(cache); - } - - [Fact] - public void OnActionExecuting_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile() - { - Duration = null - }); - - var context = GetActionExecutingContext(new List { cache }); - - // Act & Assert - var ex = Assert.Throws(() => cache.OnActionExecuting(context)); - Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.", - ex.Message); - } - - public static TheoryData CacheControlData - { - get - { - return new TheoryData - { - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - // If no-store is set, then location is ignored. - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Client, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - // If no-store is set, then duration is ignored. - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 100, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Client, - NoStore = false, - VaryByHeader = null - }), - "private,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = null - }), - "public,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.None, - NoStore = false, - VaryByHeader = null - }), - "no-cache,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 31536000, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = null - }), - "public,max-age=31536000" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 20, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = null - }), - "public,max-age=20" - } - }; - } - } - - [Theory] - [MemberData(nameof(CacheControlData))] - public void OnActionExecuting_CanSetCacheControlHeaders(ResponseCacheFilter cache, string output) - { - // Arrange - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); - } - - public static TheoryData NoStoreData - { - get - { - return new TheoryData - { - // If no-store is set, then location is ignored. - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Client, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = null - }), - "no-store" - }, - // If no-store is set, then duration is ignored. - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 100, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = null - }), - "no-store" - } - }; - } - } - - [Theory] - [MemberData(nameof(NoStoreData))] - public void OnActionExecuting_DoesNotSetLocationOrDuration_IfNoStoreIsSet( - ResponseCacheFilter cache, string output) - { - // Arrange - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); - } - - public static TheoryData VaryByHeaderData - { - get - { - return new TheoryData - { - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = "Accept" - }), - "Accept", - "public,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByHeader = "Accept" - }), - "Accept", - "no-store" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Client, - NoStore = false, - VaryByHeader = "Accept" - }), - "Accept", - "private,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Client, - NoStore = false, - VaryByHeader = "Test" - }), - "Test", - "private,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 31536000, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = "Test" - }), - "Test", - "public,max-age=31536000" - } - }; - } - } - - [Theory] - [MemberData(nameof(VaryByHeaderData))] - public void ResponseCacheCanSetVaryByHeader(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput) - { - // Arrange - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal(varyOutput, context.HttpContext.Response.Headers["Vary"]); - Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]); - } - - public static TheoryData VaryByQueryKeyData - { - get - { - return new TheoryData - { - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByQueryKeys = new[] { "Accept" } - }), - new[] { "Accept" }, - "public,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = true, - VaryByQueryKeys = new[] { "Accept" } - }), - new[] { "Accept" }, - "no-store" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Client, - NoStore = false, - VaryByQueryKeys = new[] { "Accept" } - }), - new[] { "Accept" }, - "private,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.Client, - NoStore = false, - VaryByQueryKeys = new[] { "Accept", "Test" } - }), - new[] { "Accept", "Test" }, - "private,max-age=10" - }, - { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 31536000, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByQueryKeys = new[] { "Accept", "Test" } - }), - new[] { "Accept", "Test" }, - "public,max-age=31536000" - } - }; - } - } - - [Theory] - [MemberData(nameof(VaryByQueryKeyData))] - public void ResponseCacheCanSetVaryByQueryKeys(ResponseCacheFilter cache, string[] varyOutput, string cacheControlOutput) - { - // Arrange - var context = GetActionExecutingContext(new List { cache }); - context.HttpContext.Features.Set(new ResponseCachingFeature()); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal(varyOutput, context.HttpContext.Features.Get().VaryByQueryKeys); - Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers[HeaderNames.CacheControl]); - } - - [Fact] - public void NonEmptyVaryByQueryKeys_WithoutConfiguringMiddleware_Throws() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.None, - NoStore = true, - VaryByHeader = null, - VaryByQueryKeys = new[] { "Test" } - }); - var context = GetActionExecutingContext(new List { cache }); - - // Act & Assert - var exception = Assert.Throws(() => cache.OnActionExecuting(context)); - Assert.Equal("'VaryByQueryKeys' requires the response cache middleware.", exception.Message); - } - - [Fact] - public void SetsPragmaOnNoCache() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.None, - NoStore = true, - VaryByHeader = null - }); - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("no-store,no-cache", context.HttpContext.Response.Headers["Cache-control"]); - Assert.Equal("no-cache", context.HttpContext.Response.Headers["Pragma"]); - } - - [Fact] - public void IsOverridden_ReturnsTrueForAllButLastFilter() - { - // Arrange - var caches = new List(); - caches.Add(new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = null - })); - caches.Add(new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, - Location = ResponseCacheLocation.Any, - NoStore = false, - VaryByHeader = null - })); - - var context = GetActionExecutingContext(caches); - - // Act & Assert - var cache = Assert.IsType(caches[0]); - Assert.True(cache.IsOverridden(context)); - cache = Assert.IsType(caches[1]); - Assert.False(cache.IsOverridden(context)); - } - - [Fact] - public void FilterDurationProperty_OverridesCachePolicySetting() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - Duration = 10 - }); - cache.Duration = 20; - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("public,max-age=20", context.HttpContext.Response.Headers["Cache-control"]); - } - - [Fact] - public void FilterLocationProperty_OverridesCachePolicySetting() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, - Location = ResponseCacheLocation.None - }); - cache.Location = ResponseCacheLocation.Client; - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("private,max-age=10", context.HttpContext.Response.Headers["Cache-control"]); - } - - [Fact] - public void FilterNoStoreProperty_OverridesCachePolicySetting() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - NoStore = true - }); - cache.NoStore = false; - cache.Duration = 10; - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("public,max-age=10", context.HttpContext.Response.Headers["Cache-control"]); - } - - [Fact] - public void FilterVaryByProperty_OverridesCachePolicySetting() - { - // Arrange - var cache = new ResponseCacheFilter( - new CacheProfile - { - NoStore = true, - VaryByHeader = "Accept" - }); - cache.VaryByHeader = "Test"; - var context = GetActionExecutingContext(new List { cache }); - - // Act - cache.OnActionExecuting(context); - - // Assert - Assert.Equal("Test", context.HttpContext.Response.Headers["Vary"]); - } - - private ActionExecutingContext GetActionExecutingContext(List filters = null) - { - return new ActionExecutingContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - filters ?? new List(), - new Dictionary(), - new object()); - } - } -} \ 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 6b0bbd55dd..6c74d876de 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1098,6 +1098,23 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.Equal(expected, response.Trim()); } + [Fact] + public async Task ResponseCacheAttributes_AreApplied() + { + // Arrange + var expected = "Hello from ModelWithResponseCache.OnGet"; + + // Act + var response = await Client.GetAsync("/ModelWithResponseCache"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var cacheControl = response.Headers.CacheControl; + Assert.Equal(TimeSpan.FromSeconds(10), cacheControl.MaxAge.Value); + Assert.True(cacheControl.Private); + Assert.Equal(expected, (await response.Content.ReadAsStringAsync()).Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs new file mode 100644 index 0000000000..0bea6bd5cc --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/ResponseCacheFilterApplicationModelProviderTest.cs @@ -0,0 +1,138 @@ +// 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.Authorization; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class ResponseCacheFilterApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_DoesNothingIfHandlerHasNoResponseCacheAttributes() + { + // Arrange + var options = new TestOptionsManager(); + var provider = new ResponseCacheFilterApplicationModelProvider(options); + var typeInfo = typeof(PageWithoutResponseCache).GetTypeInfo(); + var context = GetApplicationProviderContext(typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Empty(context.PageApplicationModel.Filters); + } + + private class PageWithoutResponseCache : Page + { + public ModelWithoutResponseCache Model => null; + + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + [Authorize] + public class ModelWithoutResponseCache : PageModel + { + public void OnGet() + { + } + } + + [Fact] + public void OnProvidersExecuting_AddsResponseCacheFilters() + { + // Arrange + var options = new TestOptionsManager(); + var provider = new ResponseCacheFilterApplicationModelProvider(options); + var typeInfo = typeof(PageWithResponseCache).GetTypeInfo(); + var context = GetApplicationProviderContext(typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + f => { }, + f => + { + var filter = Assert.IsType(f); + Assert.Equal("Abc", filter.VaryByHeader); + Assert.Equal(12, filter.Duration); + Assert.True(filter.NoStore); + }); + } + + private class PageWithResponseCache : Page + { + public ModelWithResponseCache Model => null; + + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + [ResponseCache(Duration = 12, NoStore = true, VaryByHeader = "Abc")] + private class ModelWithResponseCache : PageModel + { + public virtual void OnGet() + { + } + } + + [Fact] + public void OnProvidersExecuting_ReadsCacheProfileFromOptions() + { + // Arrange + var options = new TestOptionsManager(); + options.Value.CacheProfiles.Add("TestCacheProfile", new CacheProfile + { + Duration = 14, + VaryByQueryKeys = new[] { "A" }, + }); + var provider = new ResponseCacheFilterApplicationModelProvider(options); + var typeInfo = typeof(PageWithResponseCacheProfile).GetTypeInfo(); + var context = GetApplicationProviderContext(typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + f => { }, + f => + { + var filter = Assert.IsType(f); + Assert.Equal(new[] { "A" }, filter.VaryByQueryKeys); + Assert.Equal(14, filter.Duration); + }); + } + + private class PageWithResponseCacheProfile : Page + { + public ModelWithResponseCacheProfile Model => null; + + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + [ResponseCache(CacheProfileName = "TestCacheProfile")] + private class ModelWithResponseCacheProfile : PageModel + { + public virtual void OnGet() + { + } + } + + private static PageApplicationModelProviderContext GetApplicationProviderContext(TypeInfo typeInfo) + { + var defaultProvider = new DefaultPageApplicationModelProvider(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + defaultProvider.OnProvidersExecuting(context); + return context; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index dfdc65274c..62a53ce733 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -437,6 +437,7 @@ namespace Microsoft.AspNetCore.Mvc typeof(AuthorizationPageApplicationModelProvider), typeof(DefaultPageApplicationModelProvider), typeof(TempDataFilterPageApplicationModelProvider), + typeof(ResponseCacheFilterApplicationModelProvider), } }, }; diff --git a/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cs b/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cs new file mode 100644 index 0000000000..d60240490d --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cs @@ -0,0 +1,21 @@ +// 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.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + [ResponseCache(Duration = 10, Location = ResponseCacheLocation.Client)] + public class ModelWithResponseCache : PageModel + { + public string Message { get; set; } + + public void OnGet() + { + Message = $"Hello from {nameof(ModelWithResponseCache)}.{nameof(OnGet)}"; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cshtml b/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cshtml new file mode 100644 index 0000000000..5883b6056f --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithResponseCache.cshtml @@ -0,0 +1,4 @@ +@page +@model RazorPagesWebSite.ModelWithResponseCache + +@Model.Message