Issue #1785 - Changes to add CacheProfiles for response caching.
This commit is contained in:
parent
6e8cc6ba74
commit
4691823a50
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a set of settings which can be used for response caching.
|
||||
/// </summary>
|
||||
public class CacheProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the duration in seconds for which the response is cached.
|
||||
/// If this property is set to a non null value,
|
||||
/// the "max-age" in "Cache-control" header is set in the <see cref="HttpContext.Response" />.
|
||||
/// </summary>
|
||||
public int? Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location where the data from a particular URL must be cached.
|
||||
/// If this property is set to a non null value,
|
||||
/// the "Cache-control" header is set in the <see cref="HttpContext.Response" />.
|
||||
/// </summary>
|
||||
public ResponseCacheLocation? Location { get; set; }
|
||||
|
||||
/// <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 in
|
||||
/// <see cref="HttpContext.Response" /> to "no-store".
|
||||
/// Ignores the "Location" parameter for values other than "None".
|
||||
/// Ignores the "Duration" parameter.
|
||||
/// </summary>
|
||||
public bool? NoStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the Vary header in <see cref="HttpContext.Response" />.
|
||||
/// </summary>
|
||||
public string VaryByHeader { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,28 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ActionFilterAttribute"/> which sets the appropriate headers related to Response caching.
|
||||
/// Specifies the parameters necessary for setting appropriate headers in response caching.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class ResponseCacheAttribute : ActionFilterAttribute, IResponseCacheFilter
|
||||
public class ResponseCacheAttribute : Attribute, IFilterFactory, IOrderedFilter
|
||||
{
|
||||
// A nullable-int cannot be used as an Attribute parameter.
|
||||
// Hence this nullable-int is present to back the Duration property.
|
||||
// The same goes for nullable-ResponseCacheLocation and nullable-bool.
|
||||
private int? _duration;
|
||||
private ResponseCacheLocation? _location;
|
||||
private bool? _noStore;
|
||||
|
||||
/// <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
|
||||
|
|
@ -38,97 +40,85 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// <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 _location ?? ResponseCacheLocation.Any;
|
||||
}
|
||||
set
|
||||
{
|
||||
_location = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value which determines whether the data should be stored or not.
|
||||
/// When set to true, it sets "Cache-control" header to "no-store".
|
||||
/// 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; set; }
|
||||
public bool NoStore
|
||||
{
|
||||
get
|
||||
{
|
||||
return _noStore ?? false;
|
||||
}
|
||||
set
|
||||
{
|
||||
_noStore = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the Vary response header.
|
||||
/// </summary>
|
||||
public string VaryByHeader { get; set; }
|
||||
|
||||
// <inheritdoc />
|
||||
public override void OnActionExecuting([NotNull] ActionExecutingContext context)
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the cache profile name.
|
||||
/// </summary>
|
||||
public string CacheProfileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The order of the filter.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
public IFilter CreateInstance([NotNull] IServiceProvider serviceProvider)
|
||||
{
|
||||
// If there are more filters which can override the values written by this filter,
|
||||
// then skip execution of this filter.
|
||||
if (IsOverridden(context))
|
||||
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||
|
||||
CacheProfile selectedProfile = null;
|
||||
if (CacheProfileName != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var headers = context.HttpContext.Response.Headers;
|
||||
|
||||
// Clear all headers
|
||||
headers.Remove("Vary");
|
||||
headers.Remove("Cache-control");
|
||||
headers.Remove("Pragma");
|
||||
|
||||
if (!string.IsNullOrEmpty(VaryByHeader))
|
||||
{
|
||||
headers.Set("Vary", VaryByHeader);
|
||||
}
|
||||
|
||||
if (NoStore)
|
||||
{
|
||||
headers.Set("Cache-control", "no-store");
|
||||
|
||||
// Cache-control: no-store, no-cache is valid.
|
||||
if (Location == ResponseCacheLocation.None)
|
||||
optionsAccessor.Options.CacheProfiles.TryGetValue(CacheProfileName, out selectedProfile);
|
||||
if (selectedProfile == null)
|
||||
{
|
||||
headers.Append("Cache-control", "no-cache");
|
||||
headers.Set("Pragma", "no-cache");
|
||||
throw new InvalidOperationException(Resources.FormatCacheProfileNotFound(CacheProfileName));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_duration == null)
|
||||
|
||||
// If the ResponseCacheAttribute parameters are set,
|
||||
// then it must override the values from the Cache Profile.
|
||||
// The below expression first checks if the duration is set by the attribute's parameter.
|
||||
// If absent, it checks the selected cache profile (Note: There can be no cache profile as well)
|
||||
// The same is the case for other properties.
|
||||
_duration = _duration ?? selectedProfile?.Duration;
|
||||
_noStore = _noStore ?? selectedProfile?.NoStore;
|
||||
_location = _location ?? selectedProfile?.Location;
|
||||
VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader;
|
||||
|
||||
// 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
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration)));
|
||||
}
|
||||
|
||||
string cacheControlValue = null;
|
||||
switch (Location)
|
||||
{
|
||||
case ResponseCacheLocation.Any:
|
||||
cacheControlValue = "public";
|
||||
break;
|
||||
case ResponseCacheLocation.Client:
|
||||
cacheControlValue = "private";
|
||||
break;
|
||||
case ResponseCacheLocation.None:
|
||||
cacheControlValue = "no-cache";
|
||||
headers.Set("Pragma", "no-cache");
|
||||
break;
|
||||
}
|
||||
|
||||
cacheControlValue = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{1}max-age={2}",
|
||||
cacheControlValue,
|
||||
cacheControlValue != null? "," : null,
|
||||
Duration);
|
||||
|
||||
if (cacheControlValue != null)
|
||||
{
|
||||
headers.Set("Cache-control", cacheControlValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// internal for Unit Testing purposes.
|
||||
internal bool IsOverridden([NotNull] ActionExecutingContext 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;
|
||||
Duration = _duration,
|
||||
Location = _location,
|
||||
NoStore = _noStore,
|
||||
VaryByHeader = VaryByHeader
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ActionFilterAttribute"/> which sets the appropriate headers related to response caching.
|
||||
/// </summary>
|
||||
public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location where the data from a particular URL must be cached.
|
||||
/// </summary>
|
||||
public ResponseCacheLocation Location { get; set; }
|
||||
|
||||
/// <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; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the Vary response header.
|
||||
/// </summary>
|
||||
public string VaryByHeader { get; set; }
|
||||
|
||||
// <inheritdoc />
|
||||
public void OnActionExecuting([NotNull] ActionExecutingContext context)
|
||||
{
|
||||
// If there are more filters which can override the values written by this filter,
|
||||
// then skip execution of this filter.
|
||||
if (IsOverridden(context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var headers = context.HttpContext.Response.Headers;
|
||||
|
||||
// Clear all headers
|
||||
headers.Remove("Vary");
|
||||
headers.Remove("Cache-control");
|
||||
headers.Remove("Pragma");
|
||||
|
||||
if (!string.IsNullOrEmpty(VaryByHeader))
|
||||
{
|
||||
headers.Set("Vary", VaryByHeader);
|
||||
}
|
||||
|
||||
if (NoStore)
|
||||
{
|
||||
headers.Set("Cache-control", "no-store");
|
||||
|
||||
// Cache-control: no-store, no-cache is valid.
|
||||
if (Location == ResponseCacheLocation.None)
|
||||
{
|
||||
headers.Append("Cache-control", "no-cache");
|
||||
headers.Set("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.Set("Pragma", "no-cache");
|
||||
break;
|
||||
}
|
||||
|
||||
cacheControlValue = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{1}max-age={2}",
|
||||
cacheControlValue,
|
||||
cacheControlValue != null? "," : null,
|
||||
Duration);
|
||||
|
||||
if (cacheControlValue != null)
|
||||
{
|
||||
headers.Set("Cache-control", cacheControlValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <inheritdoc />
|
||||
public void OnActionExecuted([NotNull]ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
// internal for Unit Testing purposes.
|
||||
internal bool IsOverridden([NotNull] ActionExecutingContext 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||
|
|
@ -79,11 +80,11 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="ExcludeValidationDescriptor"/> which are used to construct a list
|
||||
/// of exclude filters by <see cref="IValidationExcludeFiltersProvider"/>,
|
||||
/// of exclude filters by <see cref="IValidationExcludeFiltersProvider"/>.
|
||||
/// </summary>
|
||||
public List<ExcludeValidationDescriptor> ValidationExcludeFilters { get; }
|
||||
= new List<ExcludeValidationDescriptor>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
||||
/// errors are ignored.
|
||||
|
|
@ -139,5 +140,12 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// when it contains the media type */*. <see langword="false"/> by default.
|
||||
/// </summary>
|
||||
public bool RespectBrowserAcceptHeader { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Dictionary of CacheProfile Names, <see cref="CacheProfile"/> which are pre-defined settings for
|
||||
/// <see cref="ResponseCacheFilter"/>.
|
||||
/// </summary>
|
||||
public Dictionary<string, CacheProfile> CacheProfiles { get; }
|
||||
= new Dictionary<string, CacheProfile>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
= new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// The argument '{0}' is invalid. Media types containing wildcards (*) are not supported.
|
||||
/// The argument '{0}' is invalid. Media types which match all types or match all subtypes are not supported.
|
||||
/// </summary>
|
||||
internal static string MatchAllContentTypeIsNotAllowed
|
||||
{
|
||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The argument '{0}' is invalid. Media types containing wildcards (*) are not supported.
|
||||
/// The argument '{0}' is invalid. Media types which match all types or match all subtypes are not supported.
|
||||
/// </summary>
|
||||
internal static string FormatMatchAllContentTypeIsNotAllowed(object p0)
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The content-type '{0}' added in the '{1}' property is invalid. Media types containing wildcards (*) are not supported.
|
||||
/// The content-type '{0}' added in the '{1}' property is invalid. Media types which match all types or match all subtypes are not supported.
|
||||
/// </summary>
|
||||
internal static string ObjectResult_MatchAllContentType
|
||||
{
|
||||
|
|
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The content-type '{0}' added in the '{1}' property is invalid. Media types containing wildcards (*) are not supported.
|
||||
/// The content-type '{0}' added in the '{1}' property is invalid. Media types which match all types or match all subtypes are not supported.
|
||||
/// </summary>
|
||||
internal static string FormatObjectResult_MatchAllContentType(object p0, object p1)
|
||||
{
|
||||
|
|
@ -562,24 +562,6 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
get { return GetString("Common_ValueNotValidForProperty"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter
|
||||
/// mappings.
|
||||
/// </summary>
|
||||
internal static string FormatterMappings_NotValidMediaType
|
||||
{
|
||||
get { return GetString("FormatterMappings_NotValidMediaType"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed
|
||||
/// with a '.' character.
|
||||
/// </summary>
|
||||
internal static string Format_NotValid
|
||||
{
|
||||
get { return GetString("Format_NotValid"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is invalid.
|
||||
/// </summary>
|
||||
|
|
@ -1756,6 +1738,38 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.
|
||||
/// </summary>
|
||||
internal static string FormatterMappings_NotValidMediaType
|
||||
{
|
||||
get { return GetString("FormatterMappings_NotValidMediaType"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.
|
||||
/// </summary>
|
||||
internal static string FormatFormatterMappings_NotValidMediaType(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("FormatterMappings_NotValidMediaType"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character.
|
||||
/// </summary>
|
||||
internal static string Format_NotValid
|
||||
{
|
||||
get { return GetString("Format_NotValid"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character.
|
||||
/// </summary>
|
||||
internal static string FormatFormat_NotValid(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("Format_NotValid"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No URL for remote validation could be found.
|
||||
/// </summary>
|
||||
|
|
@ -1788,6 +1802,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_RemoteValidationFailed"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' cache profile is not defined.
|
||||
/// </summary>
|
||||
internal static string CacheProfileNotFound
|
||||
{
|
||||
get { return GetString("CacheProfileNotFound"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' cache profile is not defined.
|
||||
/// </summary>
|
||||
internal static string FormatCacheProfileNotFound(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("CacheProfileNotFound"), p0);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@
|
|||
</data>
|
||||
<data name="ApiExplorer_UnsupportedAction" xml:space="preserve">
|
||||
<value>The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.</value>
|
||||
</data>
|
||||
</data>
|
||||
<data name="FormatterMappings_NotValidMediaType" xml:space="preserve">
|
||||
<value>The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.</value>
|
||||
</data>
|
||||
|
|
@ -463,4 +463,7 @@
|
|||
<data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve">
|
||||
<value>'{0}' is invalid.</value>
|
||||
</data>
|
||||
<data name="CacheProfileNotFound" xml:space="preserve">
|
||||
<value>The '{0}' cache profile is not defined.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class ResponseCacheAttributeTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Cache20Sec")]
|
||||
// To verify case-insensitive lookup.
|
||||
[InlineData("cache20sec")]
|
||||
public void CreateInstance_SelectsTheAppropriateCacheProfile(string profileName)
|
||||
{
|
||||
// Arrange
|
||||
var responseCache = new ResponseCacheAttribute() {
|
||||
CacheProfileName = profileName
|
||||
};
|
||||
var cacheProfiles = new Dictionary<string, CacheProfile>();
|
||||
cacheProfiles.Add("Cache20Sec", new CacheProfile { NoStore = true });
|
||||
cacheProfiles.Add("Test", new CacheProfile { Duration = 20 });
|
||||
|
||||
// Act
|
||||
var createdFilter = responseCache.CreateInstance(GetServiceProvider(cacheProfiles));
|
||||
|
||||
// Assert
|
||||
var responseCacheFilter = Assert.IsType<ResponseCacheFilter>(createdFilter);
|
||||
Assert.True(responseCacheFilter.NoStore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_ThrowsIfThereAreNoMatchingCacheProfiles()
|
||||
{
|
||||
// Arrange
|
||||
var responseCache = new ResponseCacheAttribute()
|
||||
{
|
||||
CacheProfileName = "HelloWorld"
|
||||
};
|
||||
var cacheProfiles = new Dictionary<string, CacheProfile>();
|
||||
cacheProfiles.Add("Cache20Sec", new CacheProfile { NoStore = true });
|
||||
cacheProfiles.Add("Test", new CacheProfile { Duration = 20 });
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => responseCache.CreateInstance(GetServiceProvider(cacheProfiles)));
|
||||
Assert.Equal("The 'HelloWorld' cache profile is not defined.", ex.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> OverrideData
|
||||
{
|
||||
get
|
||||
{
|
||||
// When there are no cache profiles then the passed in data is returned unchanged
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" },
|
||||
null,
|
||||
new CacheProfile
|
||||
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" }
|
||||
};
|
||||
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{ Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null },
|
||||
null,
|
||||
new CacheProfile
|
||||
{ Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null }
|
||||
};
|
||||
|
||||
// Everything gets overriden if attribute parameters are present,
|
||||
// when a particular cache profile is chosen.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{
|
||||
Duration = 20,
|
||||
Location = ResponseCacheLocation.Any,
|
||||
NoStore = false,
|
||||
VaryByHeader = "Accept",
|
||||
CacheProfileName = "TestCacheProfile"
|
||||
},
|
||||
new Dictionary<string, CacheProfile> { { "TestCacheProfile", new CacheProfile
|
||||
{
|
||||
Duration = 10,
|
||||
Location = ResponseCacheLocation.Client,
|
||||
NoStore = true,
|
||||
VaryByHeader = "Test"
|
||||
} } },
|
||||
new CacheProfile
|
||||
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" }
|
||||
};
|
||||
|
||||
// Select parameters override the selected profile.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{
|
||||
Duration = 534,
|
||||
CacheProfileName = "TestCacheProfile"
|
||||
},
|
||||
new Dictionary<string, CacheProfile>() { { "TestCacheProfile", new CacheProfile
|
||||
{
|
||||
Duration = 10,
|
||||
Location = ResponseCacheLocation.Client,
|
||||
NoStore = false,
|
||||
VaryByHeader = "Test"
|
||||
} } },
|
||||
new CacheProfile
|
||||
{ Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test" }
|
||||
};
|
||||
|
||||
// Duration parameter gets added to the selected profile.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{
|
||||
Duration = 534,
|
||||
CacheProfileName = "TestCacheProfile"
|
||||
},
|
||||
new Dictionary<string, CacheProfile>() { { "TestCacheProfile", new CacheProfile
|
||||
{
|
||||
Location = ResponseCacheLocation.Client,
|
||||
NoStore = false,
|
||||
VaryByHeader = "Test"
|
||||
} } },
|
||||
new CacheProfile
|
||||
{ Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test" }
|
||||
};
|
||||
|
||||
// Default values gets added for parameters which are absent
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute()
|
||||
{
|
||||
Duration = 5234,
|
||||
CacheProfileName = "TestCacheProfile"
|
||||
},
|
||||
new Dictionary<string, CacheProfile>() { { "TestCacheProfile", new CacheProfile() } },
|
||||
new CacheProfile
|
||||
{ Duration = 5234, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(OverrideData))]
|
||||
public void CreateInstance_HonorsOverrides(
|
||||
ResponseCacheAttribute responseCache,
|
||||
Dictionary<string, CacheProfile> cacheProfiles,
|
||||
CacheProfile expectedProfile)
|
||||
{
|
||||
// Arrange & Act
|
||||
var createdFilter = responseCache.CreateInstance(GetServiceProvider(cacheProfiles));
|
||||
|
||||
// Assert
|
||||
var responseCacheFilter = Assert.IsType<ResponseCacheFilter>(createdFilter);
|
||||
Assert.Equal(expectedProfile.Duration, responseCacheFilter.Duration);
|
||||
Assert.Equal(expectedProfile.Location, responseCacheFilter.Location);
|
||||
Assert.Equal(expectedProfile.NoStore, responseCacheFilter.NoStore);
|
||||
Assert.Equal(expectedProfile.VaryByHeader, responseCacheFilter.VaryByHeader);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_ThrowsWhenTheDurationIsNotSet_WithNoStoreFalse()
|
||||
{
|
||||
// Arrange
|
||||
var responseCache = new ResponseCacheAttribute()
|
||||
{
|
||||
CacheProfileName = "Test"
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
private IServiceProvider GetServiceProvider(Dictionary<string, CacheProfile> cacheProfiles)
|
||||
{
|
||||
var serviceProvider = new Mock<IServiceProvider>();
|
||||
var optionsAccessor = new Mock<IOptions<MvcOptions>>();
|
||||
var options = new MvcOptions();
|
||||
if (cacheProfiles != null)
|
||||
{
|
||||
foreach (var p in cacheProfiles)
|
||||
{
|
||||
options.CacheProfiles.Add(p.Key, p.Value);
|
||||
}
|
||||
}
|
||||
|
||||
optionsAccessor.SetupGet(o => o.Options).Returns(options);
|
||||
serviceProvider
|
||||
.Setup(s => s.GetService(typeof(IOptions<MvcOptions>)))
|
||||
.Returns(optionsAccessor.Object);
|
||||
|
||||
return serviceProvider.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http.Core;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
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<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResponseCacheFilter_ThrowsIfDurationIsNotSet_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.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CacheControlData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 100, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.Client,
|
||||
NoStore = false, VaryByHeader = null
|
||||
}),
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null
|
||||
}),
|
||||
"public,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.None, NoStore = false, VaryByHeader = null
|
||||
}),
|
||||
"no-cache,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 31536000, Location = ResponseCacheLocation.Any,
|
||||
NoStore = false, VaryByHeader = null
|
||||
}),
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
yield return new object[] {
|
||||
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<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> NoStoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null
|
||||
}),
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
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<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> VaryData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.Any,
|
||||
NoStore = false, VaryByHeader = "Accept"
|
||||
}),
|
||||
"Accept",
|
||||
"public,max-age=10" };
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 0, Location= ResponseCacheLocation.Any,
|
||||
NoStore = true, VaryByHeader = "Accept"
|
||||
}),
|
||||
"Accept",
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.Client,
|
||||
NoStore = false, VaryByHeader = "Accept"
|
||||
}),
|
||||
"Accept",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 10, Location = ResponseCacheLocation.Client,
|
||||
NoStore = false, VaryByHeader = "Test"
|
||||
}),
|
||||
"Test",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheFilter(
|
||||
new CacheProfile
|
||||
{
|
||||
Duration = 31536000, Location = ResponseCacheLocation.Any,
|
||||
NoStore = false, VaryByHeader = "Test"
|
||||
}),
|
||||
"Test",
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(VaryData))]
|
||||
public void ResponseCacheCanSetVary(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(varyOutput, context.HttpContext.Response.Headers.Get("Vary"));
|
||||
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[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<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store,no-cache", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers.Get("Pragma"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsOverridden_ReturnsTrueForAllButLastFilter()
|
||||
{
|
||||
// Arrange
|
||||
var caches = new List<IFilter>();
|
||||
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));
|
||||
}
|
||||
|
||||
private ActionExecutingContext GetActionExecutingContext(List<IFilter> filters = null)
|
||||
{
|
||||
return new ActionExecutingContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
filters ?? new List<IFilter>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,5 +30,18 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => options.MaxModelValidationErrors = -1);
|
||||
Assert.Equal("value", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenMultipleCacheProfilesWithSameNameAreAdded()
|
||||
{
|
||||
// Arrange
|
||||
var options = new MvcOptions();
|
||||
options.CacheProfiles.Add("HelloWorld", new CacheProfile { Duration = 10 });
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<ArgumentException>(
|
||||
() => options.CacheProfiles.Add("HelloWorld", new CacheProfile { Duration = 5 }));
|
||||
Assert.Equal("An item with the same key has already been added.", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Http.Core;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class ResponseCacheAttributeTest
|
||||
{
|
||||
[Fact]
|
||||
public void OnActionExecuting_DoesNotThrow_WhenNoStoreIsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new ResponseCacheAttribute() { NoStore = true };
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnActionExecuting_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new ResponseCacheAttribute() { NoStore = false };
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => { cache.OnActionExecuting(context); });
|
||||
Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CacheControlData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { new ResponseCacheAttribute { NoStore = true, Duration = 0 }, "no-store" };
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute
|
||||
{ NoStore = true, Duration = 0, Location = ResponseCacheLocation.Client },
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 0, Location = ResponseCacheLocation.Any },
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 100 }, "no-store"
|
||||
};
|
||||
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Client, Duration = 10 },
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Any, Duration = 10 },
|
||||
"public,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.None, Duration = 10 },
|
||||
"no-cache,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Client, Duration = 10 },
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Any, Duration = 31536000 },
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Duration = 20 },
|
||||
"public,max-age=20"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CacheControlData))]
|
||||
public void OnActionExecuting_CanSetCacheControlHeaders(ResponseCacheAttribute cache, string output)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> NoStoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute
|
||||
{ NoStore = true, Location = ResponseCacheLocation.Client, Duration = 0 },
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Location = ResponseCacheLocation.Any, Duration = 0 },
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 100 }, "no-store"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NoStoreData))]
|
||||
public void OnActionExecuting_DoesNotSetLocationOrDuration_IfNoStoreIsSet(
|
||||
ResponseCacheAttribute cache, string output)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> VaryData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { VaryByHeader = "Accept", Duration = 10 },
|
||||
"Accept",
|
||||
"public,max-age=10" };
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { VaryByHeader = "Accept", NoStore = true, Duration = 0 },
|
||||
"Accept",
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Client, Duration = 10, VaryByHeader = "Accept"
|
||||
},
|
||||
"Accept",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Any, Duration = 10, VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"public,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Client, Duration = 10, VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Any,
|
||||
Duration = 31536000,
|
||||
VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(VaryData))]
|
||||
public void ResponseCacheCanSetVary(ResponseCacheAttribute cache, string varyOutput, string cacheControlOutput)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(varyOutput, context.HttpContext.Response.Headers.Get("Vary"));
|
||||
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetsPragmaOnNoCache()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new ResponseCacheAttribute()
|
||||
{
|
||||
NoStore = true,
|
||||
Location = ResponseCacheLocation.None,
|
||||
Duration = 0
|
||||
};
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store,no-cache", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers.Get("Pragma"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsOverridden_ReturnsTrueForAllButLastFilter()
|
||||
{
|
||||
// Arrange
|
||||
var caches = new List<IFilter>();
|
||||
caches.Add(new ResponseCacheAttribute());
|
||||
caches.Add(new ResponseCacheAttribute());
|
||||
|
||||
var context = GetActionExecutingContext(caches);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True((caches[0] as ResponseCacheAttribute).IsOverridden(context));
|
||||
Assert.False((caches[1] as ResponseCacheAttribute).IsOverridden(context));
|
||||
}
|
||||
|
||||
private ActionExecutingContext GetActionExecutingContext(List<IFilter> filters = null)
|
||||
{
|
||||
return new ActionExecutingContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
filters ?? new List<IFilter>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +128,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Equal("Accept", data);
|
||||
data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("public, max-age=10", data);
|
||||
Assert.Throws<InvalidOperationException>(() => response.Headers.GetValues("Pragma"));
|
||||
IEnumerable<string> pragmaValues;
|
||||
response.Headers.TryGetValues("Pragma", out pragmaValues);
|
||||
Assert.Null(pragmaValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -154,8 +156,111 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
var client = server.CreateClient();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => client.GetAsync("http://localhost/CacheHeaders/ThrowsWhenDurationIsNotSet"));
|
||||
Assert.Equal(
|
||||
"If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
// Cache Profiles
|
||||
[Fact]
|
||||
public async Task ResponseCache_SetsAllHeaders_FromCacheProfile()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/CacheProfiles/PublicCache30Sec");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("public, max-age=30", data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseCache_SetsAllHeaders_ChosesTheRightProfile()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/CacheProfiles/PrivateCache30Sec");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("max-age=30, private", data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseCache_SetsNoCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/CacheProfiles/NoCache");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("no-store, no-cache", data);
|
||||
data = Assert.Single(response.Headers.GetValues("Pragma"));
|
||||
Assert.Equal("no-cache", data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseCache_AddsHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/CacheProfiles/CacheProfileAddParameter");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("public, max-age=30", data);
|
||||
data = Assert.Single(response.Headers.GetValues("Vary"));
|
||||
Assert.Equal("Accept", data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseCache_ModifiesHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/CacheProfiles/CacheProfileOverride");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("public, max-age=10", data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseCacheAttribute_OnAction_OverridesTheValuesOnClass()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/ClassLevelNoStore/CacheThisActionWithProfileSettings");
|
||||
|
||||
// Assert
|
||||
var data = Assert.Single(response.Headers.GetValues("Vary"));
|
||||
Assert.Equal("Accept", data);
|
||||
data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||
Assert.Equal("public, max-age=30", data);
|
||||
IEnumerable<string> pragmaValues;
|
||||
response.Headers.TryGetValues("Pragma", out pragmaValues);
|
||||
Assert.Null(pragmaValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,36 +7,42 @@ namespace ResponseCacheWebSite
|
|||
{
|
||||
public class CacheHeadersController : Controller
|
||||
{
|
||||
[HttpGet("/CacheHeaders/Index")]
|
||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/PublicCache")]
|
||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any)]
|
||||
public IActionResult PublicCache()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/ClientCache")]
|
||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Client)]
|
||||
public IActionResult ClientCache()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/NoStore")]
|
||||
[ResponseCache(NoStore = true, Duration = 0)]
|
||||
public IActionResult NoStore()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/NoCacheAtAll")]
|
||||
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
||||
public IActionResult NoCacheAtAll()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/SetHeadersInAction")]
|
||||
[ResponseCache(Duration = 40)]
|
||||
public IActionResult SetHeadersInAction()
|
||||
{
|
||||
|
|
@ -44,12 +50,14 @@ namespace ResponseCacheWebSite
|
|||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/SetsCacheControlPublicByDefault")]
|
||||
[ResponseCache(Duration = 40)]
|
||||
public IActionResult SetsCacheControlPublicByDefault()
|
||||
{
|
||||
return Content("Hello World!");
|
||||
}
|
||||
|
||||
[HttpGet("/CacheHeaders/ThrowsWhenDurationIsNotSet")]
|
||||
[ResponseCache(VaryByHeader = "Accept")]
|
||||
public IActionResult ThrowsWhenDurationIsNotSet()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Mvc;
|
||||
|
||||
namespace ResponseCacheWebSite.Controllers
|
||||
{
|
||||
public class CacheProfilesController
|
||||
{
|
||||
[HttpGet("/CacheProfiles/PublicCache30Sec")]
|
||||
[ResponseCache(CacheProfileName = "PublicCache30Sec")]
|
||||
public string PublicCache30Sec()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
[HttpGet("/CacheProfiles/PrivateCache30Sec")]
|
||||
[ResponseCache(CacheProfileName = "PrivateCache30Sec")]
|
||||
public string PrivateCache30Sec()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
[HttpGet("/CacheProfiles/NoCache")]
|
||||
[ResponseCache(CacheProfileName = "NoCache")]
|
||||
public string NoCache()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
[HttpGet("/CacheProfiles/CacheProfileAddParameter")]
|
||||
[ResponseCache(CacheProfileName = "PublicCache30Sec", VaryByHeader = "Accept")]
|
||||
public string CacheProfileAddParameter()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
[HttpGet("/CacheProfiles/CacheProfileOverride")]
|
||||
[ResponseCache(CacheProfileName = "PublicCache30Sec", Duration = 10)]
|
||||
public string CacheProfileOverride()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,22 +8,26 @@ namespace ResponseCacheWebSite
|
|||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")]
|
||||
public class ClassLevelCacheController
|
||||
{
|
||||
[HttpGet("/ClassLevelCache/GetHelloWorld")]
|
||||
public string GetHelloWorld()
|
||||
{
|
||||
return "Hello, World!";
|
||||
}
|
||||
|
||||
[HttpGet("/ClassLevelCache/GetFooBar")]
|
||||
public string GetFooBar()
|
||||
{
|
||||
return "Foo Bar!";
|
||||
}
|
||||
|
||||
[HttpGet("/ClassLevelCache/ConflictExistingHeader")]
|
||||
[ResponseCache(Duration = 20)]
|
||||
public string ConflictExistingHeader()
|
||||
{
|
||||
return "Conflict";
|
||||
}
|
||||
|
||||
[HttpGet("/ClassLevelCache/DoNotCacheThisAction")]
|
||||
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
||||
public string DoNotCacheThisAction()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,15 +8,24 @@ namespace ResponseCacheWebSite
|
|||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None, Duration = 0)]
|
||||
public class ClassLevelNoStoreController
|
||||
{
|
||||
[HttpGet("/ClassLevelNoStore/GetHelloWorld")]
|
||||
public string GetHelloWorld()
|
||||
{
|
||||
return "Hello, World!";
|
||||
}
|
||||
|
||||
[HttpGet("/ClassLevelNoStore/CacheThisAction")]
|
||||
[ResponseCache(VaryByHeader = "Accept", Duration = 10)]
|
||||
public string CacheThisAction()
|
||||
{
|
||||
return "Conflict";
|
||||
}
|
||||
|
||||
[HttpGet("/ClassLevelNoStore/CacheThisActionWithProfileSettings")]
|
||||
[ResponseCache(CacheProfileName = "PublicCache30Sec", VaryByHeader = "Accept")]
|
||||
public string CacheThisActionWithProfileSettings()
|
||||
{
|
||||
return "Conflict";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace ResponseCacheWebSite
|
||||
|
|
@ -11,10 +12,33 @@ namespace ResponseCacheWebSite
|
|||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
var configuration = app.GetTestConfiguration();
|
||||
|
||||
app.UseServices(services =>
|
||||
{
|
||||
services.AddMvc(configuration);
|
||||
services.Configure<MvcOptions>(options =>
|
||||
{
|
||||
options.CacheProfiles.Add(
|
||||
"PublicCache30Sec", new CacheProfile
|
||||
{
|
||||
Duration = 30,
|
||||
Location = ResponseCacheLocation.Any
|
||||
});
|
||||
|
||||
options.CacheProfiles.Add(
|
||||
"PrivateCache30Sec", new CacheProfile
|
||||
{
|
||||
Duration = 30,
|
||||
Location = ResponseCacheLocation.Client
|
||||
});
|
||||
|
||||
options.CacheProfiles.Add(
|
||||
"NoCache", new CacheProfile
|
||||
{
|
||||
NoStore = true,
|
||||
Duration = 0,
|
||||
Location = ResponseCacheLocation.None
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.UseMvc(routes =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue