diff --git a/Mvc.sln b/Mvc.sln
index 914ebd1fbe..52f976e5b0 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.22410.0
+VisualStudioVersion = 14.0.22416.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@@ -118,6 +118,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorPageMiddlewareWebSite"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CustomRouteWebSite", "test\WebSites\CustomRouteWebSite\CustomRouteWebSite.kproj", "{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCacheWebSite", "test\WebSites\ResponseCacheWebSite\ResponseCacheWebSite.kproj", "{BDEEBE09-C0C4-433C-B0B8-8478C9776996}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -654,6 +656,18 @@ Global
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|x86.ActiveCfg = Release|Any CPU
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|x86.Build.0 = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Debug|x86.Build.0 = Debug|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.ActiveCfg = Release|Any CPU
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -711,5 +725,6 @@ Global
{0AD78AB5-D67C-49BC-81B1-0C51BFA82B5E} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
+ {BDEEBE09-C0C4-433C-B0B8-8478C9776996} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IResponseCacheFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IResponseCacheFilter.cs
new file mode 100644
index 0000000000..dc895b2eac
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IResponseCacheFilter.cs
@@ -0,0 +1,12 @@
+// 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
+{
+ ///
+ /// An which sets the appropriate headers related to Response caching.
+ ///
+ public interface IResponseCacheFilter : IActionFilter
+ {
+ }
+}
\ 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
new file mode 100644
index 0000000000..691d6c7a55
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs
@@ -0,0 +1,134 @@
+// 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.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class ResponseCacheAttribute : ActionFilterAttribute, IResponseCacheFilter
+ {
+ // A nullable-int cannot be used as an Attribute parameter.
+ // Hence this nullable-int is present to back the Duration property.
+ private int? _duration;
+
+ ///
+ /// 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
+ {
+ return _duration ?? 0;
+ }
+ set
+ {
+ _duration = value;
+ }
+ }
+
+ ///
+ /// 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 true, 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 override 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
+ {
+ if (_duration == null)
+ {
+ 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;
+ }
+ }
+}
\ 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 dbc9de9fe4..e102501345 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1546,6 +1546,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("AsyncResourceFilter_InvalidShortCircuit"), p0, p1, p2, p3);
}
+ ///
+ /// If the '{0}' property is not set to true, '{1}' property must be specified.
+ ///
+ internal static string ResponseCache_SpecifyDuration
+ {
+ get { return GetString("ResponseCache_SpecifyDuration"); }
+ }
+
+ ///
+ /// If the '{0}' property is not set to true, '{1}' property must be specified.
+ ///
+ internal static string FormatResponseCache_SpecifyDuration(object p0, object p1)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("ResponseCache_SpecifyDuration"), p0, p1);
+ }
+
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 b42eda3c40..ccc5ba71be 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -415,4 +415,7 @@
If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}.
+
+ If the '{0}' property is not set to true, '{1}' property must be specified.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ResponseCacheLocation.cs b/src/Microsoft.AspNet.Mvc.Core/ResponseCacheLocation.cs
new file mode 100644
index 0000000000..0dfeefefca
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ResponseCacheLocation.cs
@@ -0,0 +1,26 @@
+// 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
+{
+ ///
+ /// Determines the value for the "Cache-control" header in the response.
+ ///
+ public enum ResponseCacheLocation
+ {
+ ///
+ /// Cached in both proxies and client.
+ /// Sets "Cache-control" header to "public".
+ ///
+ Any = 0,
+ ///
+ /// Cached only in the client.
+ /// Sets "Cache-control" header to "private".
+ ///
+ Client = 1,
+ ///
+ /// "Cache-control" and "Pragma" headers are set to "no-cache".
+ ///
+ None = 2
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs
new file mode 100644
index 0000000000..76e9489198
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs
@@ -0,0 +1,242 @@
+// 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 { 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 { cache });
+
+ // Act & Assert
+ var exception = Assert.Throws(() => { cache.OnActionExecuting(context); });
+ Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
+ exception.Message);
+ }
+
+ public static IEnumerable