Add support for ResponseCache in Razor Pages

Fixes #6437
This commit is contained in:
Pranav K 2017-08-03 10:02:23 -07:00
parent 3a710c3d64
commit 900a5c7c4c
14 changed files with 1105 additions and 713 deletions

View File

@ -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
{
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>());

View File

@ -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)
{
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -437,6 +437,7 @@ namespace Microsoft.AspNetCore.Mvc
typeof(AuthorizationPageApplicationModelProvider),
typeof(DefaultPageApplicationModelProvider),
typeof(TempDataFilterPageApplicationModelProvider),
typeof(ResponseCacheFilterApplicationModelProvider),
}
},
};

View File

@ -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)}";
}
}
}

View File

@ -0,0 +1,4 @@
@page
@model RazorPagesWebSite.ModelWithResponseCache
@Model.Message