Changes introducing ResponseCache to cache content in the client/proxy.
This commit is contained in:
parent
d2fe1ebad7
commit
262bb9a732
17
Mvc.sln
17
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionFilter"/> which sets the appropriate headers related to Response caching.
|
||||
/// </summary>
|
||||
public interface IResponseCacheFilter : IActionFilter
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ActionFilterAttribute"/> which sets the appropriate headers related to Response caching.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration in seconds for which the response is cached.
|
||||
/// This is a required parameter.
|
||||
/// This sets "max-age" in "Cache-control" header.
|
||||
/// </summary>
|
||||
public int Duration
|
||||
{
|
||||
get
|
||||
{
|
||||
return _duration ?? 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
_duration = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location where the data from a particular URL must be cached.
|
||||
/// </summary>
|
||||
public ResponseCacheLocation Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value which determines whether the data should be stored or not.
|
||||
/// When set to true, it sets "Cache-control" header to "no-store".
|
||||
/// Ignores the "Location" parameter for values other than "None".
|
||||
/// Ignores the "duration" parameter.
|
||||
/// </summary>
|
||||
public bool NoStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the Vary response header.
|
||||
/// </summary>
|
||||
public string VaryByHeader { get; set; }
|
||||
|
||||
// <inheritdoc />
|
||||
public 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<IResponseCacheFilter>().Last() != this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1546,6 +1546,22 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("AsyncResourceFilter_InvalidShortCircuit"), p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the '{0}' property is not set to true, '{1}' property must be specified.
|
||||
/// </summary>
|
||||
internal static string ResponseCache_SpecifyDuration
|
||||
{
|
||||
get { return GetString("ResponseCache_SpecifyDuration"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the '{0}' property is not set to true, '{1}' property must be specified.
|
||||
/// </summary>
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -415,4 +415,7 @@
|
|||
<data name="AsyncResourceFilter_InvalidShortCircuit" xml:space="preserve">
|
||||
<value>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}.</value>
|
||||
</data>
|
||||
<data name="ResponseCache_SpecifyDuration" xml:space="preserve">
|
||||
<value>If the '{0}' property is not set to true, '{1}' property must be specified.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the value for the "Cache-control" header in the response.
|
||||
/// </summary>
|
||||
public enum ResponseCacheLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached in both proxies and client.
|
||||
/// Sets "Cache-control" header to "public".
|
||||
/// </summary>
|
||||
Any = 0,
|
||||
/// <summary>
|
||||
/// Cached only in the client.
|
||||
/// Sets "Cache-control" header to "private".
|
||||
/// </summary>
|
||||
Client = 1,
|
||||
/// <summary>
|
||||
/// "Cache-control" and "Pragma" headers are set to "no-cache".
|
||||
/// </summary>
|
||||
None = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnActionExecuting_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new ResponseCacheAttribute() { NoStore = false };
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => { cache.OnActionExecuting(context); });
|
||||
Assert.Equal("If the 'NoStore' property is not set to true, 'Duration' property must be specified.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CacheControlData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { new ResponseCacheAttribute { NoStore = true, Duration = 0 }, "no-store" };
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute
|
||||
{ NoStore = true, Duration = 0, Location = ResponseCacheLocation.Client },
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 0, Location = ResponseCacheLocation.Any },
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 100 }, "no-store"
|
||||
};
|
||||
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Client, Duration = 10 },
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Any, Duration = 10 },
|
||||
"public,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.None, Duration = 10 },
|
||||
"no-cache,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Client, Duration = 10 },
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Location = ResponseCacheLocation.Any, Duration = 31536000 },
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { Duration = 20 },
|
||||
"public,max-age=20"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CacheControlData))]
|
||||
public void OnActionExecuting_CanSetCacheControlHeaders(ResponseCacheAttribute cache, string output)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> NoStoreData
|
||||
{
|
||||
get
|
||||
{
|
||||
// If no-store is set, then location is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute
|
||||
{ NoStore = true, Location = ResponseCacheLocation.Client, Duration = 0 },
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Location = ResponseCacheLocation.Any, Duration = 0 },
|
||||
"no-store"
|
||||
};
|
||||
// If no-store is set, then duration is ignored.
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { NoStore = true, Duration = 100 }, "no-store"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NoStoreData))]
|
||||
public void OnActionExecuting_DoesNotSetLocationOrDuration_IfNoStoreIsSet(
|
||||
ResponseCacheAttribute cache, string output)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(output, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> VaryData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { VaryByHeader = "Accept", Duration = 10 },
|
||||
"Accept",
|
||||
"public,max-age=10" };
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute { VaryByHeader = "Accept", NoStore = true, Duration = 0 },
|
||||
"Accept",
|
||||
"no-store"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Client, Duration = 10, VaryByHeader = "Accept"
|
||||
},
|
||||
"Accept",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Any, Duration = 10, VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"public,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Client, Duration = 10, VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"private,max-age=10"
|
||||
};
|
||||
yield return new object[] {
|
||||
new ResponseCacheAttribute {
|
||||
Location = ResponseCacheLocation.Any,
|
||||
Duration = 31536000,
|
||||
VaryByHeader = "Test"
|
||||
},
|
||||
"Test",
|
||||
"public,max-age=31536000"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(VaryData))]
|
||||
public void ResponseCacheCanSetVary(ResponseCacheAttribute cache, string varyOutput, string cacheControlOutput)
|
||||
{
|
||||
// Arrange
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(varyOutput, context.HttpContext.Response.Headers.Get("Vary"));
|
||||
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetsPragmaOnNoCache()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new ResponseCacheAttribute()
|
||||
{
|
||||
NoStore = true,
|
||||
Location = ResponseCacheLocation.None,
|
||||
Duration = 0
|
||||
};
|
||||
var context = GetActionExecutingContext(new List<IFilter> { cache });
|
||||
|
||||
// Act
|
||||
cache.OnActionExecuting(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("no-store,no-cache", context.HttpContext.Response.Headers.Get("Cache-control"));
|
||||
Assert.Equal("no-cache", context.HttpContext.Response.Headers.Get("Pragma"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsOverridden_ReturnsTrueForAllButLastFilter()
|
||||
{
|
||||
// Arrange
|
||||
var caches = new List<IFilter>();
|
||||
caches.Add(new ResponseCacheAttribute());
|
||||
caches.Add(new ResponseCacheAttribute());
|
||||
|
||||
var context = GetActionExecutingContext(caches);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True((caches[0] as ResponseCacheAttribute).IsOverridden(context));
|
||||
Assert.False((caches[1] as ResponseCacheAttribute).IsOverridden(context));
|
||||
}
|
||||
|
||||
private ActionExecutingContext GetActionExecutingContext(List<IFilter> filters = null)
|
||||
{
|
||||
return new ActionExecutingContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
filters ?? new List<IFilter>(),
|
||||
new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IApplicationBuilder> _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<object[]> 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<InvalidOperationException>(() => 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<InvalidOperationException>(
|
||||
() => client.GetAsync("http://localhost/CacheHeaders/ThrowsWhenDurationIsNotSet"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>bdeebe09-c0c4-433c-b0b8-8478c9776996</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>63072</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue