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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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.
|
// A nullable-int cannot be used as an Attribute parameter.
|
||||||
// Hence this nullable-int is present to back the Duration property.
|
// Hence this nullable-int is present to back the Duration property.
|
||||||
|
// The same goes for nullable-ResponseCacheLocation and nullable-bool.
|
||||||
private int? _duration;
|
private int? _duration;
|
||||||
|
private ResponseCacheLocation? _location;
|
||||||
|
private bool? _noStore;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the duration in seconds for which the response is cached.
|
/// 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.
|
/// This sets "max-age" in "Cache-control" header.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Duration
|
public int Duration
|
||||||
|
|
@ -38,97 +40,85 @@ namespace Microsoft.AspNet.Mvc
|
||||||
/// <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 _location ?? ResponseCacheLocation.Any;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_location = 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.
|
||||||
/// 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 "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 _noStore ?? false;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_noStore = 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; set; }
|
||||||
|
|
||||||
// <inheritdoc />
|
/// <summary>
|
||||||
public override void OnActionExecuting([NotNull] ActionExecutingContext context)
|
/// 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,
|
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
|
||||||
// then skip execution of this filter.
|
|
||||||
if (IsOverridden(context))
|
CacheProfile selectedProfile = null;
|
||||||
|
if (CacheProfileName != null)
|
||||||
{
|
{
|
||||||
return;
|
optionsAccessor.Options.CacheProfiles.TryGetValue(CacheProfileName, out selectedProfile);
|
||||||
}
|
if (selectedProfile == null)
|
||||||
|
|
||||||
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");
|
throw new InvalidOperationException(Resources.FormatCacheProfileNotFound(CacheProfileName));
|
||||||
headers.Set("Pragma", "no-cache");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
// If the ResponseCacheAttribute parameters are set,
|
||||||
if (_duration == null)
|
// 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(
|
Duration = _duration,
|
||||||
Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration)));
|
Location = _location,
|
||||||
}
|
NoStore = _noStore,
|
||||||
|
VaryByHeader = VaryByHeader
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNet.Mvc.ApplicationModels;
|
using Microsoft.AspNet.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||||
|
|
@ -79,11 +80,11 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of <see cref="ExcludeValidationDescriptor"/> which are used to construct a list
|
/// 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>
|
/// </summary>
|
||||||
public List<ExcludeValidationDescriptor> ValidationExcludeFilters { get; }
|
public List<ExcludeValidationDescriptor> ValidationExcludeFilters { get; }
|
||||||
= new List<ExcludeValidationDescriptor>();
|
= new List<ExcludeValidationDescriptor>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
|
||||||
/// errors are ignored.
|
/// errors are ignored.
|
||||||
|
|
@ -139,5 +140,12 @@ namespace Microsoft.AspNet.Mvc
|
||||||
/// when it contains the media type */*. <see langword="false"/> by default.
|
/// when it contains the media type */*. <see langword="false"/> by default.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool RespectBrowserAcceptHeader { get; set; } = false;
|
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);
|
= new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static string MatchAllContentTypeIsNotAllowed
|
internal static string MatchAllContentTypeIsNotAllowed
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static string FormatMatchAllContentTypeIsNotAllowed(object p0)
|
internal static string FormatMatchAllContentTypeIsNotAllowed(object p0)
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static string ObjectResult_MatchAllContentType
|
internal static string ObjectResult_MatchAllContentType
|
||||||
{
|
{
|
||||||
|
|
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal static string FormatObjectResult_MatchAllContentType(object p0, object p1)
|
internal static string FormatObjectResult_MatchAllContentType(object p0, object p1)
|
||||||
{
|
{
|
||||||
|
|
@ -562,24 +562,6 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
get { return GetString("Common_ValueNotValidForProperty"); }
|
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>
|
/// <summary>
|
||||||
/// The value '{0}' is invalid.
|
/// The value '{0}' is invalid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1756,6 +1738,38 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0);
|
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>
|
/// <summary>
|
||||||
/// No URL for remote validation could be found.
|
/// No URL for remote validation could be found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1788,6 +1802,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_RemoteValidationFailed"), p0);
|
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)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@
|
||||||
</data>
|
</data>
|
||||||
<data name="ApiExplorer_UnsupportedAction" xml:space="preserve">
|
<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>
|
<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">
|
<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>
|
<value>The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -463,4 +463,7 @@
|
||||||
<data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve">
|
<data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve">
|
||||||
<value>'{0}' is invalid.</value>
|
<value>'{0}' is invalid.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CacheProfileNotFound" xml:space="preserve">
|
||||||
|
<value>The '{0}' cache profile is not defined.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</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);
|
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => options.MaxModelValidationErrors = -1);
|
||||||
Assert.Equal("value", ex.ParamName);
|
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);
|
Assert.Equal("Accept", data);
|
||||||
data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
data = Assert.Single(response.Headers.GetValues("Cache-control"));
|
||||||
Assert.Equal("public, max-age=10", data);
|
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]
|
[Fact]
|
||||||
|
|
@ -154,8 +156,111 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => client.GetAsync("http://localhost/CacheHeaders/ThrowsWhenDurationIsNotSet"));
|
() => 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
|
public class CacheHeadersController : Controller
|
||||||
{
|
{
|
||||||
|
[HttpGet("/CacheHeaders/Index")]
|
||||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")]
|
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/PublicCache")]
|
||||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any)]
|
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any)]
|
||||||
public IActionResult PublicCache()
|
public IActionResult PublicCache()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/ClientCache")]
|
||||||
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Client)]
|
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Client)]
|
||||||
public IActionResult ClientCache()
|
public IActionResult ClientCache()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/NoStore")]
|
||||||
[ResponseCache(NoStore = true, Duration = 0)]
|
[ResponseCache(NoStore = true, Duration = 0)]
|
||||||
public IActionResult NoStore()
|
public IActionResult NoStore()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/NoCacheAtAll")]
|
||||||
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
||||||
public IActionResult NoCacheAtAll()
|
public IActionResult NoCacheAtAll()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/SetHeadersInAction")]
|
||||||
[ResponseCache(Duration = 40)]
|
[ResponseCache(Duration = 40)]
|
||||||
public IActionResult SetHeadersInAction()
|
public IActionResult SetHeadersInAction()
|
||||||
{
|
{
|
||||||
|
|
@ -44,12 +50,14 @@ namespace ResponseCacheWebSite
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/SetsCacheControlPublicByDefault")]
|
||||||
[ResponseCache(Duration = 40)]
|
[ResponseCache(Duration = 40)]
|
||||||
public IActionResult SetsCacheControlPublicByDefault()
|
public IActionResult SetsCacheControlPublicByDefault()
|
||||||
{
|
{
|
||||||
return Content("Hello World!");
|
return Content("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/CacheHeaders/ThrowsWhenDurationIsNotSet")]
|
||||||
[ResponseCache(VaryByHeader = "Accept")]
|
[ResponseCache(VaryByHeader = "Accept")]
|
||||||
public IActionResult ThrowsWhenDurationIsNotSet()
|
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")]
|
[ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")]
|
||||||
public class ClassLevelCacheController
|
public class ClassLevelCacheController
|
||||||
{
|
{
|
||||||
|
[HttpGet("/ClassLevelCache/GetHelloWorld")]
|
||||||
public string GetHelloWorld()
|
public string GetHelloWorld()
|
||||||
{
|
{
|
||||||
return "Hello, World!";
|
return "Hello, World!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/ClassLevelCache/GetFooBar")]
|
||||||
public string GetFooBar()
|
public string GetFooBar()
|
||||||
{
|
{
|
||||||
return "Foo Bar!";
|
return "Foo Bar!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/ClassLevelCache/ConflictExistingHeader")]
|
||||||
[ResponseCache(Duration = 20)]
|
[ResponseCache(Duration = 20)]
|
||||||
public string ConflictExistingHeader()
|
public string ConflictExistingHeader()
|
||||||
{
|
{
|
||||||
return "Conflict";
|
return "Conflict";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/ClassLevelCache/DoNotCacheThisAction")]
|
||||||
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
|
||||||
public string DoNotCacheThisAction()
|
public string DoNotCacheThisAction()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,24 @@ namespace ResponseCacheWebSite
|
||||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None, Duration = 0)]
|
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None, Duration = 0)]
|
||||||
public class ClassLevelNoStoreController
|
public class ClassLevelNoStoreController
|
||||||
{
|
{
|
||||||
|
[HttpGet("/ClassLevelNoStore/GetHelloWorld")]
|
||||||
public string GetHelloWorld()
|
public string GetHelloWorld()
|
||||||
{
|
{
|
||||||
return "Hello, World!";
|
return "Hello, World!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/ClassLevelNoStore/CacheThisAction")]
|
||||||
[ResponseCache(VaryByHeader = "Accept", Duration = 10)]
|
[ResponseCache(VaryByHeader = "Accept", Duration = 10)]
|
||||||
public string CacheThisAction()
|
public string CacheThisAction()
|
||||||
{
|
{
|
||||||
return "Conflict";
|
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.
|
// 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.Builder;
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
namespace ResponseCacheWebSite
|
namespace ResponseCacheWebSite
|
||||||
|
|
@ -11,10 +12,33 @@ namespace ResponseCacheWebSite
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
var configuration = app.GetTestConfiguration();
|
var configuration = app.GetTestConfiguration();
|
||||||
|
|
||||||
app.UseServices(services =>
|
app.UseServices(services =>
|
||||||
{
|
{
|
||||||
services.AddMvc(configuration);
|
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 =>
|
app.UseMvc(routes =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue