Updated the ResponseCacheFilter class to store a reference to the CacheProfile passed into the contructor

Updated the ResponseCacheFilter Duration, Location, NoStore, and VaryByHeader properties to return one of 3 values: the specific setting applied to the instance, the setting supplied by the CachePolicy (from the constructor), or a default value.  The emphasis being not to change the outward function of these properties to consumers, but to defer the evaluation of these properties until the OnActionExecuting method.
Updated the ResponseCacheFilter to check whether a cache duration has been supplied when the NoStore property is false and throw an InvalidOperationException when a duration has not been provided.
Updated ResponseCacheFilterTest to reflect the changes in behavior in the ResponseCacheFilter
Updated the ResponseCacheFilterAttributeTest to reflect the change in the behavior of the ResponseCacheFilter

Added unit tests to ResponseCacheFilterTest.cs to verify that the CachePolicy properties are properly overriden with the properties set directly on the ResponseCacheFilter.

Added functional tests to test the ability to override CacheProfile settings with properties on the ResponseCacheAttribute
This commit is contained in:
CodingGorilla 2015-05-27 00:30:50 -04:00 committed by Ryan Nowak
parent 4f419eee55
commit 70b56f157c
6 changed files with 280 additions and 36 deletions

View File

@ -14,6 +14,12 @@ namespace Microsoft.AspNet.Mvc
/// </summary> /// </summary>
public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter
{ {
private readonly CacheProfile _cacheProfile;
private int? _cacheDuration;
private ResponseCacheLocation? _cacheLocation;
private bool? _cacheNoStore;
private string _cacheVaryByHeader;
/// <summary> /// <summary>
/// Creates a new instance of <see cref="ResponseCacheFilter"/> /// Creates a new instance of <see cref="ResponseCacheFilter"/>
/// </summary> /// </summary>
@ -21,20 +27,7 @@ namespace Microsoft.AspNet.Mvc
/// <see cref="ResponseCacheFilter"/>.</param> /// <see cref="ResponseCacheFilter"/>.</param>
public ResponseCacheFilter(CacheProfile cacheProfile) public ResponseCacheFilter(CacheProfile cacheProfile)
{ {
if (!(cacheProfile.NoStore ?? false)) _cacheProfile = cacheProfile;
{
// Duration MUST be set (either in the cache profile or in the attribute) unless NoStore is true.
if (cacheProfile.Duration == null)
{
throw new InvalidOperationException(
Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration)));
}
}
Duration = cacheProfile.Duration ?? 0;
Location = cacheProfile.Location ?? ResponseCacheLocation.Any;
NoStore = cacheProfile.NoStore ?? false;
VaryByHeader = cacheProfile.VaryByHeader;
} }
/// <summary> /// <summary>
@ -42,12 +35,20 @@ namespace Microsoft.AspNet.Mvc
/// This is a required parameter. /// This is a required parameter.
/// This sets "max-age" in "Cache-control" header. /// This sets "max-age" in "Cache-control" header.
/// </summary> /// </summary>
public int Duration { get; set; } public int Duration
{
get { return (_cacheDuration ?? _cacheProfile.Duration) ?? 0; }
set { _cacheDuration = value; }
}
/// <summary> /// <summary>
/// Gets or sets the location where the data from a particular URL must be cached. /// Gets or sets the location where the data from a particular URL must be cached.
/// </summary> /// </summary>
public ResponseCacheLocation Location { get; set; } public ResponseCacheLocation Location
{
get { return (_cacheLocation ?? _cacheProfile.Location) ?? ResponseCacheLocation.Any; }
set { _cacheLocation = value; }
}
/// <summary> /// <summary>
/// Gets or sets the value which determines whether the data should be stored or not. /// Gets or sets the value which determines whether the data should be stored or not.
@ -55,13 +56,21 @@ namespace Microsoft.AspNet.Mvc
/// Ignores the "Location" parameter for values other than "None". /// Ignores the "Location" parameter for values other than "None".
/// Ignores the "duration" parameter. /// Ignores the "duration" parameter.
/// </summary> /// </summary>
public bool NoStore { get; set; } public bool NoStore
{
get { return (_cacheNoStore ?? _cacheProfile.NoStore) ?? false; }
set { _cacheNoStore = value; }
}
/// <summary> /// <summary>
/// Gets or sets the value for the Vary response header. /// Gets or sets the value for the Vary response header.
/// </summary> /// </summary>
public string VaryByHeader { get; set; } public string VaryByHeader
{
get { return _cacheVaryByHeader ?? _cacheProfile.VaryByHeader; }
set { _cacheVaryByHeader = value; }
}
// <inheritdoc /> // <inheritdoc />
public void OnActionExecuting([NotNull] ActionExecutingContext context) public void OnActionExecuting([NotNull] ActionExecutingContext context)
{ {
@ -72,6 +81,16 @@ namespace Microsoft.AspNet.Mvc
return; 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; var headers = context.HttpContext.Response.Headers;
// Clear all headers // Clear all headers

View File

@ -162,7 +162,7 @@ namespace Microsoft.AspNet.Mvc
} }
[Fact] [Fact]
public void CreateInstance_ThrowsWhenTheDurationIsNotSet_WithNoStoreFalse() public void CreateInstance_DoesNotThrowWhenTheDurationIsNotSet_WithNoStoreFalse()
{ {
// Arrange // Arrange
var responseCache = new ResponseCacheAttribute() var responseCache = new ResponseCacheAttribute()
@ -172,12 +172,11 @@ namespace Microsoft.AspNet.Mvc
var cacheProfiles = new Dictionary<string, CacheProfile>(); var cacheProfiles = new Dictionary<string, CacheProfile>();
cacheProfiles.Add("Test", new CacheProfile { NoStore = false }); cacheProfiles.Add("Test", new CacheProfile { NoStore = false });
// Act & Assert // Act
var ex = Assert.Throws<InvalidOperationException>( var filter = responseCache.CreateInstance(GetServiceProvider(cacheProfiles));
() => responseCache.CreateInstance(GetServiceProvider(cacheProfiles)));
Assert.Equal( // Assert
"If the 'NoStore' property is not set to true, 'Duration' property must be specified.", Assert.NotNull(filter);
ex.Message);
} }
private IServiceProvider GetServiceProvider(Dictionary<string, CacheProfile> cacheProfiles) private IServiceProvider GetServiceProvider(Dictionary<string, CacheProfile> cacheProfiles)

View File

@ -31,17 +31,34 @@ namespace Microsoft.AspNet.Mvc
} }
[Fact] [Fact]
public void ResponseCacheFilter_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse() public void ResponseCacheFilter_DoesNotThrowIfDurationIsNotSet_WhenNoStoreIsFalse()
{ {
// Arrange, Act & Assert // Arrange, Act
var ex = Assert.Throws<InvalidOperationException>( var cache = new ResponseCacheFilter(
() => new ResponseCacheFilter( new CacheProfile
new CacheProfile {
{ Duration = null
Duration = null });
}));
Assert.Equal( // Assert
"If the 'NoStore' property is not set to true, 'Duration' property must be specified.", Assert.NotNull(cache);
}
[Fact]
public void OnActionExecuting_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse()
{
// Arrange
var cache = new ResponseCacheFilter(
new CacheProfile()
{
Duration = null
});
var context = GetActionExecutingContext(new List<IFilter> { 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); ex.Message);
} }
@ -306,6 +323,85 @@ namespace Microsoft.AspNet.Mvc
Assert.False(cache.IsOverridden(context)); 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<IFilter> { cache });
// Act
cache.OnActionExecuting(context);
// Assert
Assert.Equal("public,max-age=20", context.HttpContext.Response.Headers.Get("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<IFilter> { cache });
// Act
cache.OnActionExecuting(context);
// Assert
Assert.Equal("private,max-age=10", context.HttpContext.Response.Headers.Get("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<IFilter> { cache });
// Act
cache.OnActionExecuting(context);
// Assert
Assert.Equal("public,max-age=10", context.HttpContext.Response.Headers.Get("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<IFilter> { cache });
// Act
cache.OnActionExecuting(context);
// Assert
Assert.Equal("Test", context.HttpContext.Response.Headers.Get("Vary"));
}
private ActionExecutingContext GetActionExecutingContext(List<IFilter> filters = null) private ActionExecutingContext GetActionExecutingContext(List<IFilter> filters = null)
{ {
return new ActionExecutingContext( return new ActionExecutingContext(

View File

@ -280,5 +280,84 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
response.Headers.TryGetValues("Pragma", out pragmaValues); response.Headers.TryGetValues("Pragma", out pragmaValues);
Assert.Null(pragmaValues); Assert.Null(pragmaValues);
} }
// Cache profile overrides
[Fact]
public async Task ResponseCacheAttribute_OverridesProfileDuration_FromAttributeProperty()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/CacheProfileOverrides/PublicCache30SecTo15Sec");
// Assert
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
Assert.Equal("public, max-age=15", data);
}
[Fact]
public async Task ResponseCacheAttribute_OverridesProfileLocation_FromAttributeProperty()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/CacheProfileOverrides/PublicCache30SecToPrivateCache");
// Assert
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
Assert.Equal("max-age=30, private", data);
}
[Fact]
public async Task ResponseCacheAttribute_OverridesProfileNoStore_FromAttributeProperty()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/CacheProfileOverrides/PublicCache30SecToNoStore");
// Assert
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
Assert.Equal("no-store", data);
}
[Fact]
public async Task ResponseCacheAttribute_OverridesProfileVaryBy_FromAttributeProperty()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/CacheProfileOverrides/PublicCache30SecWithVaryByAcceptToVaryByTest");
// Assert
var cacheControl = Assert.Single(response.Headers.GetValues("Cache-control"));
Assert.Equal("public, max-age=30", cacheControl);
var vary = Assert.Single(response.Headers.GetValues("Vary"));
Assert.Equal("Test", vary);
}
[Fact]
public async Task ResponseCacheAttribute_OverridesProfileVaryBy_FromAttributeProperty_AndRemovesVaryHeader()
{
// Arrange
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/CacheProfileOverrides/PublicCache30SecWithVaryByAcceptToVaryByNone");
// Assert
var cacheControl = Assert.Single(response.Headers.GetValues("Cache-control"));
Assert.Equal("public, max-age=30", cacheControl);
Assert.Throws<InvalidOperationException>(() => response.Headers.GetValues("Vary"));
}
} }
} }

View File

@ -0,0 +1,43 @@
using System;
using Microsoft.AspNet.Mvc;
namespace ResponseCacheWebSite.Controllers
{
public class CacheProfilesOverridesController
{
[HttpGet("/CacheProfileOverrides/PublicCache30SecTo15Sec")]
[ResponseCache(CacheProfileName = "PublicCache30Sec", Duration = 15)]
public string PublicCache30SecTo15Sec()
{
return "Hello World!";
}
[HttpGet("/CacheProfileOverrides/PublicCache30SecToPrivateCache")]
[ResponseCache(CacheProfileName = "PublicCache30Sec", Location = ResponseCacheLocation.Client)]
public string PublicCache30SecToPrivateCache()
{
return "Hello World!";
}
[HttpGet("/CacheProfileOverrides/PublicCache30SecToNoStore")]
[ResponseCache(CacheProfileName = "PublicCache30Sec", NoStore = true)]
public string PublicCache30SecToNoStore()
{
return "Hello World!";
}
[HttpGet("/CacheProfileOverrides/PublicCache30SecWithVaryByAcceptToVaryByTest")]
[ResponseCache(CacheProfileName = "PublicCache30Sec", VaryByHeader = "Test")]
public string PublicCache30SecWithVaryByAcceptToVaryByTest()
{
return "Hello World!";
}
[HttpGet("/CacheProfileOverrides/PublicCache30SecWithVaryByAcceptToVaryByNone")]
[ResponseCache(CacheProfileName = "PublicCache30Sec", VaryByHeader = null)]
public string PublicCache30SecWithVaryByAcceptToVaryByNone()
{
return "Hello World!";
}
}
}

View File

@ -37,6 +37,14 @@ namespace ResponseCacheWebSite
Location = ResponseCacheLocation.None Location = ResponseCacheLocation.None
}); });
options.CacheProfiles.Add(
"PublicCache30SecVaryByAcceptHeader", new CacheProfile
{
Duration = 30,
Location = ResponseCacheLocation.Any,
VaryByHeader = "Accept"
});
options.Filters.Add(new ResponseCacheFilter(new CacheProfile options.Filters.Add(new ResponseCacheFilter(new CacheProfile
{ {
NoStore = true, NoStore = true,