From 262bb9a7323b664d4b860931c04d1aea8142facd Mon Sep 17 00:00:00 2001 From: sornaks Date: Mon, 5 Jan 2015 19:23:52 -0800 Subject: [PATCH] Changes introducing ResponseCache to cache content in the client/proxy. --- Mvc.sln | 17 +- .../Filters/IResponseCacheFilter.cs | 12 + .../Filters/ResponseCacheAttribute.cs | 134 ++++++++++ .../Properties/Resources.Designer.cs | 16 ++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 3 + .../ResponseCacheLocation.cs | 26 ++ .../ResponseCacheAttributeTest.cs | 242 ++++++++++++++++++ .../ResponseCacheTest.cs | 161 ++++++++++++ .../project.json | 1 + .../Controllers/CacheHeadersController.cs | 59 +++++ .../Controllers/ClassLevelCacheController.cs | 33 +++ .../ClassLevelNoStoreController.cs | 22 ++ .../ResponseCacheWebSite.kproj | 25 ++ test/WebSites/ResponseCacheWebSite/Startup.cs | 23 ++ .../ResponseCacheWebSite/project.json | 19 ++ .../ResponseCacheWebSite/wwwroot/readme.md | Bin 0 -> 30 bytes 16 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IResponseCacheFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheAttribute.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ResponseCacheLocation.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs create mode 100644 test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs create mode 100644 test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs create mode 100644 test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs create mode 100644 test/WebSites/ResponseCacheWebSite/ResponseCacheWebSite.kproj create mode 100644 test/WebSites/ResponseCacheWebSite/Startup.cs create mode 100644 test/WebSites/ResponseCacheWebSite/project.json create mode 100644 test/WebSites/ResponseCacheWebSite/wwwroot/readme.md 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 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 { cache }); + + // Act + cache.OnActionExecuting(context); + + // Assert + Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control")); + } + + public static IEnumerable 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 { cache }); + + // Act + cache.OnActionExecuting(context); + + // Assert + Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control")); + } + + public static IEnumerable 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 { 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 { 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(); + 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 filters = null) + { + return new ActionExecutingContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), + filters ?? new List(), + new Dictionary()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs new file mode 100644 index 0000000000..d1114b1b3e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs @@ -0,0 +1,161 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using ResponseCacheWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class ResponseCacheTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices("ResponseCacheWebSite"); + private readonly Action _app = new Startup().Configure; + + [Fact] + public async Task ResponseCache_SetsAllHeaders() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/CacheHeaders/Index"); + + // Assert + var data = Assert.Single(response.Headers.GetValues("Cache-control")); + Assert.Equal("public, max-age=100", data); + data = Assert.Single(response.Headers.GetValues("Vary")); + Assert.Equal("Accept", data); + } + + public static IEnumerable CacheControlData + { + get + { + yield return new object[] { "http://localhost/CacheHeaders/PublicCache", "public, max-age=100" }; + yield return new object[] { "http://localhost/CacheHeaders/ClientCache", "max-age=100, private" }; + yield return new object[] { "http://localhost/CacheHeaders/NoStore", "no-store" }; + yield return new object[] { "http://localhost/CacheHeaders/NoCacheAtAll", "no-store, no-cache" }; + } + } + + [Theory] + [MemberData(nameof(CacheControlData))] + public async Task ResponseCache_SetsDifferentCacheControlHeaders(string url, string expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(url); + + // Assert + var data = Assert.Single(response.Headers.GetValues("Cache-control")); + Assert.Equal(expected, data); + } + + [Fact] + public async Task SetsHeadersForAllActionsOfClass() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response1 = await client.GetAsync("http://localhost/ClassLevelCache/GetHelloWorld"); + var response2 = await client.GetAsync("http://localhost/ClassLevelCache/GetFooBar"); + + // Assert + var data = Assert.Single(response1.Headers.GetValues("Cache-control")); + Assert.Equal("public, max-age=100", data); + data = Assert.Single(response1.Headers.GetValues("Vary")); + Assert.Equal("Accept", data); + + data = Assert.Single(response2.Headers.GetValues("Cache-control")); + Assert.Equal("public, max-age=100", data); + data = Assert.Single(response2.Headers.GetValues("Vary")); + Assert.Equal("Accept", data); + } + + [Fact] + public async Task HeadersSetInActionOverridesTheOnesInClass() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ClassLevelCache/ConflictExistingHeader"); + + // Assert + var data = Assert.Single(response.Headers.GetValues("Cache-control")); + Assert.Equal("public, max-age=20", data); + } + + [Fact] + public async Task HeadersToNotCacheAParticularAction() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ClassLevelCache/DoNotCacheThisAction"); + + // Assert + var data = Assert.Single(response.Headers.GetValues("Cache-control")); + Assert.Equal("no-store, no-cache", data); + } + + [Fact] + public async Task ClassLevelHeadersAreUnsetByActionLevelHeaders() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/ClassLevelNoStore/CacheThisAction"); + + // 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=10", data); + Assert.Throws(() => response.Headers.GetValues("Pragma")); + } + + [Fact] + public async Task SetsCacheControlPublicByDefault() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/CacheHeaders/SetsCacheControlPublicByDefault"); + + // Assert + var data = Assert.Single(response.Headers.GetValues("Cache-control")); + Assert.Equal("public, max-age=40", data); + } + + [Fact] + public async Task ThrowsWhenDurationIsNotSet() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act & Assert + await Assert.ThrowsAsync( + () => client.GetAsync("http://localhost/CacheHeaders/ThrowsWhenDurationIsNotSet")); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 81eeb63311..3d07166528 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -27,6 +27,7 @@ "RazorInstrumentationWebsite": "1.0.0", "RazorViewEngineOptionsWebsite": "1.0.0", "RequestServicesWebSite": "1.0.0", + "ResponseCacheWebSite": "1.0.0", "TagHelperSample.Web": "1.0.0", "TagHelpersWebSite": "1.0.0", "UrlHelperWebSite": "1.0.0", diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs new file mode 100644 index 0000000000..359b592575 --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs @@ -0,0 +1,59 @@ +// 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 +{ + public class CacheHeadersController : Controller + { + [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")] + public IActionResult Index() + { + return Content("Hello World!"); + } + + [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any)] + public IActionResult PublicCache() + { + return Content("Hello World!"); + } + + [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Client)] + public IActionResult ClientCache() + { + return Content("Hello World!"); + } + + [ResponseCache(NoStore = true, Duration = 0)] + public IActionResult NoStore() + { + return Content("Hello World!"); + } + + [ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)] + public IActionResult NoCacheAtAll() + { + return Content("Hello World!"); + } + + [ResponseCache(Duration = 40)] + public IActionResult SetHeadersInAction() + { + Response.Headers.Set("Cache-control", "max-age=10"); + return Content("Hello World!"); + } + + [ResponseCache(Duration = 40)] + public IActionResult SetsCacheControlPublicByDefault() + { + return Content("Hello World!"); + } + + [ResponseCache(VaryByHeader = "Accept")] + public IActionResult ThrowsWhenDurationIsNotSet() + { + return Content("Hello World!"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs new file mode 100644 index 0000000000..9cfaffe45b --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs @@ -0,0 +1,33 @@ +// 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 +{ + [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")] + public class ClassLevelCacheController + { + public string GetHelloWorld() + { + return "Hello, World!"; + } + + public string GetFooBar() + { + return "Foo Bar!"; + } + + [ResponseCache(Duration = 20)] + public string ConflictExistingHeader() + { + return "Conflict"; + } + + [ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)] + public string DoNotCacheThisAction() + { + return "Conflict"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs new file mode 100644 index 0000000000..a3fd8502a7 --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs @@ -0,0 +1,22 @@ +// 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 +{ + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None, Duration = 0)] + public class ClassLevelNoStoreController + { + public string GetHelloWorld() + { + return "Hello, World!"; + } + + [ResponseCache(VaryByHeader = "Accept", Duration = 10)] + public string CacheThisAction() + { + return "Conflict"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/ResponseCacheWebSite.kproj b/test/WebSites/ResponseCacheWebSite/ResponseCacheWebSite.kproj new file mode 100644 index 0000000000..fad43c1846 --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/ResponseCacheWebSite.kproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + bdeebe09-c0c4-433c-b0b8-8478c9776996 + + + + + + + 2.0 + 63072 + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Startup.cs b/test/WebSites/ResponseCacheWebSite/Startup.cs new file mode 100644 index 0000000000..ffc8f1e3ab --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/Startup.cs @@ -0,0 +1,23 @@ +// 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.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace ResponseCacheWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + }); + + app.UseMvc(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/project.json b/test/WebSites/ResponseCacheWebSite/project.json new file mode 100644 index 0000000000..464f3bbc61 --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/project.json @@ -0,0 +1,19 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "webroot": "wwwroot" +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/wwwroot/readme.md b/test/WebSites/ResponseCacheWebSite/wwwroot/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..123252603f372fb2c5fac8e1c7e3f2bec12666a9 GIT binary patch literal 30 hcmezW&x0YAAqNQa8FUyF7{Y;c5s;U{z{|kJ004r123-IE literal 0 HcmV?d00001