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