diff --git a/src/Microsoft.AspNet.Mvc.Core/CacheProfile.cs b/src/Microsoft.AspNet.Mvc.Core/CacheProfile.cs
new file mode 100644
index 0000000000..2651ac04a6
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/CacheProfile.cs
@@ -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
+{
+ ///
+ /// Defines a set of settings which can be used for response caching.
+ ///
+ public class CacheProfile
+ {
+ ///
+ /// 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 .
+ ///
+ public int? Duration { get; set; }
+
+ ///
+ /// 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 .
+ ///
+ public ResponseCacheLocation? Location { get; set; }
+
+ ///
+ /// Gets or sets the value which determines whether the data should be stored or not.
+ /// When set to , it sets "Cache-control" header in
+ /// to "no-store".
+ /// Ignores the "Location" parameter for values other than "None".
+ /// Ignores the "Duration" parameter.
+ ///
+ public bool? NoStore { get; set; }
+
+ ///
+ /// Gets or sets the value for the Vary header in .
+ ///
+ public string VaryByHeader { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs
index 691d6c7a55..d27ffdbcf4 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs
@@ -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
{
///
- /// An which sets the appropriate headers related to Response caching.
+ /// Specifies the parameters necessary for setting appropriate headers in response caching.
///
[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;
///
/// 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.
///
public int Duration
@@ -38,97 +40,85 @@ namespace Microsoft.AspNet.Mvc
///
/// Gets or sets the location where the data from a particular URL must be cached.
///
- public ResponseCacheLocation Location { get; set; }
+ public ResponseCacheLocation Location
+ {
+ get
+ {
+ return _location ?? ResponseCacheLocation.Any;
+ }
+ set
+ {
+ _location = value;
+ }
+ }
///
/// 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 , it sets "Cache-control" header to "no-store".
/// Ignores the "Location" parameter for values other than "None".
/// Ignores the "duration" parameter.
///
- public bool NoStore { get; set; }
+ public bool NoStore
+ {
+ get
+ {
+ return _noStore ?? false;
+ }
+ set
+ {
+ _noStore = value;
+ }
+ }
///
/// Gets or sets the value for the Vary response header.
///
public string VaryByHeader { get; set; }
- //
- public override void OnActionExecuting([NotNull] ActionExecutingContext context)
+ ///
+ /// Gets or sets the value of the cache profile name.
+ ///
+ public string CacheProfileName { get; set; }
+
+ ///
+ /// The order of the filter.
+ ///
+ 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>();
+
+ 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().Last() != this;
+ Duration = _duration,
+ Location = _location,
+ NoStore = _noStore,
+ VaryByHeader = VaryByHeader
+ });
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheFilter.cs
new file mode 100644
index 0000000000..5439a823cd
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheFilter.cs
@@ -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
+{
+ ///
+ /// An which sets the appropriate headers related to response caching.
+ ///
+ public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter
+ {
+ ///
+ /// Creates a new instance of
+ ///
+ /// The profile which contains the settings for
+ /// .
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ public int Duration { get; set; }
+
+ ///
+ /// Gets or sets the location where the data from a particular URL must be cached.
+ ///
+ public ResponseCacheLocation Location { get; set; }
+
+ ///
+ /// Gets or sets the value which determines whether the data should be stored or not.
+ /// When set to , it sets "Cache-control" header to "no-store".
+ /// Ignores the "Location" parameter for values other than "None".
+ /// Ignores the "duration" parameter.
+ ///
+ public bool NoStore { get; set; }
+
+ ///
+ /// Gets or sets the value for the Vary response header.
+ ///
+ public string VaryByHeader { get; set; }
+
+ //
+ 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);
+ }
+ }
+ }
+
+ //
+ 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().Last() != this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
index e362341932..84588dadb0 100644
--- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
@@ -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
///
/// Gets a list of which are used to construct a list
- /// of exclude filters by ,
+ /// of exclude filters by .
///
public List ValidationExcludeFilters { get; }
= new List();
-
+
///
/// 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 */*. by default.
///
public bool RespectBrowserAcceptHeader { get; set; } = false;
+
+ ///
+ /// Gets a Dictionary of CacheProfile Names, which are pre-defined settings for
+ /// .
+ ///
+ public Dictionary CacheProfiles { get; }
+ = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 8ae8d6c081..ba86b70e93 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc.Core
= new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly);
///
- /// 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.
///
internal static string MatchAllContentTypeIsNotAllowed
{
@@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// 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.
///
internal static string FormatMatchAllContentTypeIsNotAllowed(object p0)
{
@@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// 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.
///
internal static string ObjectResult_MatchAllContentType
{
@@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// 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.
///
internal static string FormatObjectResult_MatchAllContentType(object p0, object p1)
{
@@ -562,24 +562,6 @@ namespace Microsoft.AspNet.Mvc.Core
get { return GetString("Common_ValueNotValidForProperty"); }
}
- ///
- /// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter
- /// mappings.
- ///
- internal static string FormatterMappings_NotValidMediaType
- {
- get { return GetString("FormatterMappings_NotValidMediaType"); }
- }
-
- ///
- /// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed
- /// with a '.' character.
- ///
- internal static string Format_NotValid
- {
- get { return GetString("Format_NotValid"); }
- }
-
///
/// The value '{0}' is invalid.
///
@@ -1756,6 +1738,38 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0);
}
+ ///
+ /// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.
+ ///
+ internal static string FormatterMappings_NotValidMediaType
+ {
+ get { return GetString("FormatterMappings_NotValidMediaType"); }
+ }
+
+ ///
+ /// The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.
+ ///
+ internal static string FormatFormatterMappings_NotValidMediaType(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("FormatterMappings_NotValidMediaType"), p0);
+ }
+
+ ///
+ /// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character.
+ ///
+ internal static string Format_NotValid
+ {
+ get { return GetString("Format_NotValid"); }
+ }
+
+ ///
+ /// The format provided is invalid '{0}'. A format must be a non-empty file-extension, optionally prefixed with a '.' character.
+ ///
+ internal static string FormatFormat_NotValid(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Format_NotValid"), p0);
+ }
+
///
/// No URL for remote validation could be found.
///
@@ -1788,6 +1802,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_RemoteValidationFailed"), p0);
}
+ ///
+ /// The '{0}' cache profile is not defined.
+ ///
+ internal static string CacheProfileNotFound
+ {
+ get { return GetString("CacheProfileNotFound"); }
+ }
+
+ ///
+ /// The '{0}' cache profile is not defined.
+ ///
+ 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);
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index 66e98854e1..2c556ba58d 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -450,7 +450,7 @@
The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.
-
+
The media type "{0}" is not valid. MediaTypes containing wildcards (*) are not allowed in formatter mappings.
@@ -463,4 +463,7 @@
'{0}' is invalid.
+
+ The '{0}' cache profile is not defined.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheAttributeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheAttributeTest.cs
new file mode 100644
index 0000000000..f88e50aab2
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheAttributeTest.cs
@@ -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();
+ 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(createdFilter);
+ Assert.True(responseCacheFilter.NoStore);
+ }
+
+ [Fact]
+ public void CreateInstance_ThrowsIfThereAreNoMatchingCacheProfiles()
+ {
+ // Arrange
+ var responseCache = new ResponseCacheAttribute()
+ {
+ CacheProfileName = "HelloWorld"
+ };
+ var cacheProfiles = new Dictionary();
+ cacheProfiles.Add("Cache20Sec", new CacheProfile { NoStore = true });
+ cacheProfiles.Add("Test", new CacheProfile { Duration = 20 });
+
+ // Act
+ var ex = Assert.Throws(
+ () => responseCache.CreateInstance(GetServiceProvider(cacheProfiles)));
+ Assert.Equal("The 'HelloWorld' cache profile is not defined.", ex.Message);
+ }
+
+ public static IEnumerable