From 4691823a5060db135c5672ba5b264183c999f5b3 Mon Sep 17 00:00:00 2001 From: sornaks Date: Thu, 29 Jan 2015 14:50:01 -0800 Subject: [PATCH] Issue #1785 - Changes to add CacheProfiles for response caching. --- src/Microsoft.AspNet.Mvc.Core/CacheProfile.cs | 39 +++ .../Filters/ResponseCacheAttribute.cs | 148 ++++---- .../Filters/ResponseCacheFilter.cs | 141 ++++++++ src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs | 12 +- .../Properties/Resources.Designer.cs | 74 ++-- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 5 +- .../Filters/ResponseCacheAttributeTest.cs | 204 +++++++++++ .../Filters/ResponseCacheFilterTest.cs | 318 ++++++++++++++++++ .../MvcOptionsTests.cs | 13 + .../ResponseCacheAttributeTest.cs | 243 ------------- .../ResponseCacheTest.cs | 109 +++++- .../Controllers/CacheHeadersController.cs | 8 + .../Controllers/CacheProfilesController.cs | 45 +++ .../Controllers/ClassLevelCacheController.cs | 4 + .../ClassLevelNoStoreController.cs | 9 + test/WebSites/ResponseCacheWebSite/Startup.cs | 26 +- 16 files changed, 1048 insertions(+), 350 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/CacheProfile.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResponseCacheFilter.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheAttributeTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheFilterTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs create mode 100644 test/WebSites/ResponseCacheWebSite/Controllers/CacheProfilesController.cs 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 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 { { "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() { { "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() { { "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() { { "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 cacheProfiles, + CacheProfile expectedProfile) + { + // Arrange & Act + var createdFilter = responseCache.CreateInstance(GetServiceProvider(cacheProfiles)); + + // Assert + var responseCacheFilter = Assert.IsType(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(); + cacheProfiles.Add("Test", new CacheProfile { NoStore = false }); + + // Act & Assert + var ex = Assert.Throws( + () => 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 cacheProfiles) + { + var serviceProvider = new Mock(); + var optionsAccessor = new Mock>(); + 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))) + .Returns(optionsAccessor.Object); + + return serviceProvider.Object; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheFilterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheFilterTest.cs new file mode 100644 index 0000000000..c4736c06da --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/ResponseCacheFilterTest.cs @@ -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 { 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( + () => 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 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 { 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 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 { 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 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 { 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 { 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 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(caches[0]); + Assert.True(cache.IsOverridden(context)); + cache = Assert.IsType(caches[1]); + Assert.False(cache.IsOverridden(context)); + } + + private ActionExecutingContext GetActionExecutingContext(List filters = null) + { + return new ActionExecutingContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), + filters ?? new List(), + new Dictionary(), + new object()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/MvcOptionsTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/MvcOptionsTests.cs index 5688616085..c8b32dc66d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/MvcOptionsTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/MvcOptionsTests.cs @@ -30,5 +30,18 @@ namespace Microsoft.AspNet.Mvc.Core.Test var ex = Assert.Throws(() => options.MaxModelValidationErrors = -1); Assert.Equal("value", ex.ParamName); } + + [Fact] + public void ThrowsWhenMultipleCacheProfilesWithSameNameAreAdded() + { + // Arrange + var options = new MvcOptions(); + options.CacheProfiles.Add("HelloWorld", new CacheProfile { Duration = 10 }); + + // Act & Assert + var ex = Assert.Throws( + () => options.CacheProfiles.Add("HelloWorld", new CacheProfile { Duration = 5 })); + Assert.Equal("An item with the same key has already been added.", ex.Message); + } } } \ 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 deleted file mode 100644 index 99fea13d8d..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ResponseCacheAttributeTest.cs +++ /dev/null @@ -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 { 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(), - new object()); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs index d1114b1b3e..663474aba5 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ResponseCacheTest.cs @@ -128,7 +128,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("Accept", data); data = Assert.Single(response.Headers.GetValues("Cache-control")); Assert.Equal("public, max-age=10", data); - Assert.Throws(() => response.Headers.GetValues("Pragma")); + IEnumerable pragmaValues; + response.Headers.TryGetValues("Pragma", out pragmaValues); + Assert.Null(pragmaValues); } [Fact] @@ -154,8 +156,111 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var client = server.CreateClient(); // Act & Assert - await Assert.ThrowsAsync( + var ex = await Assert.ThrowsAsync( () => 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 pragmaValues; + response.Headers.TryGetValues("Pragma", out pragmaValues); + Assert.Null(pragmaValues); } } } \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs index 359b592575..2ed071e669 100644 --- a/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs +++ b/test/WebSites/ResponseCacheWebSite/Controllers/CacheHeadersController.cs @@ -7,36 +7,42 @@ namespace ResponseCacheWebSite { public class CacheHeadersController : Controller { + [HttpGet("/CacheHeaders/Index")] [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")] public IActionResult Index() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/PublicCache")] [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any)] public IActionResult PublicCache() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/ClientCache")] [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Client)] public IActionResult ClientCache() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/NoStore")] [ResponseCache(NoStore = true, Duration = 0)] public IActionResult NoStore() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/NoCacheAtAll")] [ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)] public IActionResult NoCacheAtAll() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/SetHeadersInAction")] [ResponseCache(Duration = 40)] public IActionResult SetHeadersInAction() { @@ -44,12 +50,14 @@ namespace ResponseCacheWebSite return Content("Hello World!"); } + [HttpGet("/CacheHeaders/SetsCacheControlPublicByDefault")] [ResponseCache(Duration = 40)] public IActionResult SetsCacheControlPublicByDefault() { return Content("Hello World!"); } + [HttpGet("/CacheHeaders/ThrowsWhenDurationIsNotSet")] [ResponseCache(VaryByHeader = "Accept")] public IActionResult ThrowsWhenDurationIsNotSet() { diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/CacheProfilesController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/CacheProfilesController.cs new file mode 100644 index 0000000000..a7f31f3c71 --- /dev/null +++ b/test/WebSites/ResponseCacheWebSite/Controllers/CacheProfilesController.cs @@ -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!"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs index 9cfaffe45b..c627813d04 100644 --- a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs +++ b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelCacheController.cs @@ -8,22 +8,26 @@ namespace ResponseCacheWebSite [ResponseCache(Duration = 100, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept")] public class ClassLevelCacheController { + [HttpGet("/ClassLevelCache/GetHelloWorld")] public string GetHelloWorld() { return "Hello, World!"; } + [HttpGet("/ClassLevelCache/GetFooBar")] public string GetFooBar() { return "Foo Bar!"; } + [HttpGet("/ClassLevelCache/ConflictExistingHeader")] [ResponseCache(Duration = 20)] public string ConflictExistingHeader() { return "Conflict"; } + [HttpGet("/ClassLevelCache/DoNotCacheThisAction")] [ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)] public string DoNotCacheThisAction() { diff --git a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs index a3fd8502a7..3284f3f3b9 100644 --- a/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs +++ b/test/WebSites/ResponseCacheWebSite/Controllers/ClassLevelNoStoreController.cs @@ -8,15 +8,24 @@ namespace ResponseCacheWebSite [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None, Duration = 0)] public class ClassLevelNoStoreController { + [HttpGet("/ClassLevelNoStore/GetHelloWorld")] public string GetHelloWorld() { return "Hello, World!"; } + [HttpGet("/ClassLevelNoStore/CacheThisAction")] [ResponseCache(VaryByHeader = "Accept", Duration = 10)] public string CacheThisAction() { return "Conflict"; } + + [HttpGet("/ClassLevelNoStore/CacheThisActionWithProfileSettings")] + [ResponseCache(CacheProfileName = "PublicCache30Sec", VaryByHeader = "Accept")] + public string CacheThisActionWithProfileSettings() + { + return "Conflict"; + } } } \ No newline at end of file diff --git a/test/WebSites/ResponseCacheWebSite/Startup.cs b/test/WebSites/ResponseCacheWebSite/Startup.cs index 271f7437cc..c97e40841a 100644 --- a/test/WebSites/ResponseCacheWebSite/Startup.cs +++ b/test/WebSites/ResponseCacheWebSite/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; using Microsoft.Framework.DependencyInjection; namespace ResponseCacheWebSite @@ -11,10 +12,33 @@ namespace ResponseCacheWebSite public void Configure(IApplicationBuilder app) { var configuration = app.GetTestConfiguration(); - app.UseServices(services => { services.AddMvc(configuration); + services.Configure(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 =>