parent
3a710c3d64
commit
900a5c7c4c
|
|
@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
|||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionFilter"/> which sets the appropriate headers related to Response caching.
|
||||
/// A filter which sets the appropriate headers related to Response caching.
|
||||
/// </summary>
|
||||
public interface IResponseCacheFilter : IActionFilter
|
||||
public interface IResponseCacheFilter : IFilterMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionFilter"/> which sets the appropriate headers related to response caching.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ResponseCacheFilter"/>
|
||||
|
|
@ -31,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// <see cref="ResponseCacheFilter"/>.</param>
|
||||
public ResponseCacheFilter(CacheProfile cacheProfile)
|
||||
{
|
||||
_cacheProfile = cacheProfile;
|
||||
_executor = new ResponseCacheFilterExecutor(cacheProfile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -41,8 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </summary>
|
||||
public int Duration
|
||||
{
|
||||
get { return (_cacheDuration ?? _cacheProfile.Duration) ?? 0; }
|
||||
set { _cacheDuration = value; }
|
||||
get => _executor.Duration;
|
||||
set => _executor.Duration = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -50,8 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </summary>
|
||||
public ResponseCacheLocation Location
|
||||
{
|
||||
get { return (_cacheLocation ?? _cacheProfile.Location) ?? ResponseCacheLocation.Any; }
|
||||
set { _cacheLocation = value; }
|
||||
get => _executor.Location;
|
||||
set => _executor.Location = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,8 +51,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </summary>
|
||||
public bool NoStore
|
||||
{
|
||||
get { return (_cacheNoStore ?? _cacheProfile.NoStore) ?? false; }
|
||||
set { _cacheNoStore = value; }
|
||||
get => _executor.NoStore;
|
||||
set => _executor.NoStore = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,8 +60,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </summary>
|
||||
public string VaryByHeader
|
||||
{
|
||||
get { return _cacheVaryByHeader ?? _cacheProfile.VaryByHeader; }
|
||||
set { _cacheVaryByHeader = value; }
|
||||
get => _executor.VaryByHeader;
|
||||
set => _executor.VaryByHeader = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -83,8 +72,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
/// </remarks>
|
||||
public string[] VaryByQueryKeys
|
||||
{
|
||||
get { return _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys; }
|
||||
set { _cacheVaryByQueryKeys = value; }
|
||||
get => _executor.VaryByQueryKeys;
|
||||
set => _executor.VaryByQueryKeys = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -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<IResponseCachingFeature>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<IResponseCacheFilter>().Last() != this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IResponseCachingFeature>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,20 +78,16 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CacheProfile"/> for this attribute.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CacheProfile GetCacheProfile(MvcOptions options)
|
||||
{
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
ServiceDescriptor.Singleton<IPageApplicationModelProvider, AuthorizationPageApplicationModelProvider>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IPageApplicationModelProvider, TempDataFilterPageApplicationModelProvider>());
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IPageApplicationModelProvider, ResponseCacheFilterApplicationModelProvider>());
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IActionInvokerProvider, PageActionInvokerProvider>());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IPageFilter"/> which sets the appropriate headers related to response caching.
|
||||
/// </summary>
|
||||
public class ResponseCacheFilter : IPageFilter, IResponseCacheFilter
|
||||
{
|
||||
private readonly ResponseCacheFilterExecutor _executor;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ResponseCacheFilter"/>
|
||||
/// </summary>
|
||||
/// <param name="cacheProfile">The profile which contains the settings for
|
||||
/// <see cref="ResponseCacheFilter"/>.</param>
|
||||
public ResponseCacheFilter(CacheProfile cacheProfile)
|
||||
{
|
||||
_executor = new ResponseCacheFilterExecutor(cacheProfile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public int Duration
|
||||
{
|
||||
get => _executor.Duration;
|
||||
set => _executor.Duration = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location where the data from a particular URL must be cached.
|
||||
/// </summary>
|
||||
public ResponseCacheLocation Location
|
||||
{
|
||||
get => _executor.Location;
|
||||
set => _executor.Location = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value which determines whether the data should be stored or not.
|
||||
/// When set to <see langword="true"/>, it sets "Cache-control" header to "no-store".
|
||||
/// Ignores the "Location" parameter for values other than "None".
|
||||
/// Ignores the "duration" parameter.
|
||||
/// </summary>
|
||||
public bool NoStore
|
||||
{
|
||||
get => _executor.NoStore;
|
||||
set => _executor.NoStore = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the Vary response header.
|
||||
/// </summary>
|
||||
public string VaryByHeader
|
||||
{
|
||||
get => _executor.VaryByHeader;
|
||||
set => _executor.VaryByHeader = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query keys to vary by.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="VaryByQueryKeys"/> requires the response cache middleware.
|
||||
/// </remarks>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MvcOptions> 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<ResponseCacheAttribute>();
|
||||
foreach (var attribute in responseCacheAttributes)
|
||||
{
|
||||
var cacheProfile = attribute.GetCacheProfile(_mvcOptions);
|
||||
context.PageApplicationModel.Filters.Add(new ResponseCacheFilter(cacheProfile));
|
||||
}
|
||||
}
|
||||
|
||||
public void OnProvidersExecuted(PageApplicationModelProviderContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvalidOperationException>(() => executor.Execute(context));
|
||||
Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
public static TheoryData<CacheProfile, string> CacheControlData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<CacheProfile, string>
|
||||
{
|
||||
{
|
||||
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<CacheProfile, string> NoStoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<CacheProfile, string>
|
||||
{
|
||||
// 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<CacheProfile, string, string> VaryByHeaderData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<CacheProfile, string, string>
|
||||
{
|
||||
{
|
||||
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<CacheProfile, string[], string> VaryByQueryKeyData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<CacheProfile, string[], string>
|
||||
{
|
||||
{
|
||||
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<IResponseCachingFeature>(new ResponseCachingFeature());
|
||||
|
||||
// Acts
|
||||
executor.Execute(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(varyOutput, context.HttpContext.Features.Get<IResponseCachingFeature>().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<InvalidOperationException>(() => 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<IFilterMetadata>
|
||||
{
|
||||
filter1,
|
||||
Mock.Of<IFilterMetadata>(),
|
||||
filter2,
|
||||
Mock.Of<IFilterMetadata>(),
|
||||
};
|
||||
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<IFilterMetadata>
|
||||
{
|
||||
Mock.Of<IFilterMetadata>(),
|
||||
filter,
|
||||
Mock.Of<IFilterMetadata>(),
|
||||
Mock.Of<IFilterMetadata>(),
|
||||
};
|
||||
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<IFilterMetadata> filters = null)
|
||||
{
|
||||
return new ActionExecutingContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
filters ?? new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IFilterMetadata> { 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<IFilterMetadata> { cache });
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.OnActionExecuting(context));
|
||||
Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
public static TheoryData<ResponseCacheFilter, string> CacheControlData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<ResponseCacheFilter, string>
|
||||
{
|
||||
{
|
||||
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<IFilterMetadata> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]);
|
||||
}
|
||||
|
||||
public static TheoryData<ResponseCacheFilter, string> NoStoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<ResponseCacheFilter, string>
|
||||
{
|
||||
// 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<IFilterMetadata> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]);
|
||||
}
|
||||
|
||||
public static TheoryData<ResponseCacheFilter, string, string> VaryByHeaderData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<ResponseCacheFilter, string, string>
|
||||
{
|
||||
{
|
||||
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<IFilterMetadata> { 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<ResponseCacheFilter, string[], string> VaryByQueryKeyData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<ResponseCacheFilter, string[], string>
|
||||
{
|
||||
{
|
||||
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<IFilterMetadata> { cache });
|
||||
context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature());
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(varyOutput, context.HttpContext.Features.Get<IResponseCachingFeature>().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<IFilterMetadata> { cache });
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => 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<IFilterMetadata> { 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<IFilterMetadata>();
|
||||
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<ResponseCacheFilter>(caches[0]);
|
||||
Assert.True(cache.IsOverridden(context));
|
||||
cache = Assert.IsType<ResponseCacheFilter>(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<IFilterMetadata> { 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<IFilterMetadata> { 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<IFilterMetadata> { 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<IFilterMetadata> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test", context.HttpContext.Response.Headers["Vary"]);
|
||||
}
|
||||
|
||||
private ActionExecutingContext GetActionExecutingContext(List<IFilterMetadata> filters = null)
|
||||
{
|
||||
return new ActionExecutingContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
filters ?? new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<MvcOptions>();
|
||||
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<MvcOptions>();
|
||||
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<ResponseCacheFilter>(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<MvcOptions>();
|
||||
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<ResponseCacheFilter>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,6 +437,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
typeof(AuthorizationPageApplicationModelProvider),
|
||||
typeof(DefaultPageApplicationModelProvider),
|
||||
typeof(TempDataFilterPageApplicationModelProvider),
|
||||
typeof(ResponseCacheFilterApplicationModelProvider),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@page
|
||||
@model RazorPagesWebSite.ModelWithResponseCache
|
||||
|
||||
@Model.Message
|
||||
Loading…
Reference in New Issue