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>
public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter
{
private readonly CacheProfile _cacheProfile;
private int? _cacheDuration;
private ResponseCacheLocation? _cacheLocation;
private bool? _cacheNoStore;
private string _cacheVaryByHeader;
/// <summary>
/// Creates a new instance of <see cref="ResponseCacheFilter"/>
/// </summary>
@ -21,20 +27,7 @@ namespace Microsoft.AspNet.Mvc
/// <see cref="ResponseCacheFilter"/>.</param>
public ResponseCacheFilter(CacheProfile cacheProfile)
{
if (!(cacheProfile.NoStore ?? false))
{
// 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;
_cacheProfile = cacheProfile;
}
/// <summary>
@ -42,12 +35,20 @@ namespace Microsoft.AspNet.Mvc
/// This is a required parameter.
/// This sets "max-age" in "Cache-control" header.
/// </summary>
public int Duration { get; set; }
public int Duration
{
get { return (_cacheDuration ?? _cacheProfile.Duration) ?? 0; }
set { _cacheDuration = value; }
}
/// <summary>
/// Gets or sets the location where the data from a particular URL must be cached.
/// </summary>
public ResponseCacheLocation Location { get; set; }
public ResponseCacheLocation Location
{
get { return (_cacheLocation ?? _cacheProfile.Location) ?? ResponseCacheLocation.Any; }
set { _cacheLocation = value; }
}
/// <summary>
/// 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 "duration" parameter.
/// </summary>
public bool NoStore { get; set; }
public bool NoStore
{
get { return (_cacheNoStore ?? _cacheProfile.NoStore) ?? false; }
set { _cacheNoStore = value; }
}
/// <summary>
/// Gets or sets the value for the Vary response header.
/// </summary>
public string VaryByHeader { get; set; }
public string VaryByHeader
{
get { return _cacheVaryByHeader ?? _cacheProfile.VaryByHeader; }
set { _cacheVaryByHeader = value; }
}
// <inheritdoc />
public void OnActionExecuting([NotNull] ActionExecutingContext context)
{
@ -72,6 +81,16 @@ namespace Microsoft.AspNet.Mvc
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

View File

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

View File

@ -31,17 +31,34 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public void ResponseCacheFilter_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse()
public void ResponseCacheFilter_DoesNotThrowIfDurationIsNotSet_WhenNoStoreIsFalse()
{
// Arrange, Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => new ResponseCacheFilter(
new CacheProfile
{
Duration = null
}));
Assert.Equal(
"If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
// 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<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);
}
@ -306,6 +323,85 @@ namespace Microsoft.AspNet.Mvc
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)
{
return new ActionExecutingContext(

View File

@ -280,5 +280,84 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
response.Headers.TryGetValues("Pragma", out 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
});
options.CacheProfiles.Add(
"PublicCache30SecVaryByAcceptHeader", new CacheProfile
{
Duration = 30,
Location = ResponseCacheLocation.Any,
VaryByHeader = "Accept"
});
options.Filters.Add(new ResponseCacheFilter(new CacheProfile
{
NoStore = true,