diff --git a/sample/WebApi.OutputCache.V2.Demo/Program.cs b/sample/WebApi.OutputCache.V2.Demo/Program.cs index 32eee55..bc95816 100644 --- a/sample/WebApi.OutputCache.V2.Demo/Program.cs +++ b/sample/WebApi.OutputCache.V2.Demo/Program.cs @@ -9,7 +9,8 @@ class Program { static void Main(string[] args) { - var config = new HttpSelfHostConfiguration("http://localhost:999"); + const string hostUrl = "http://localhost:999"; + var config = new HttpSelfHostConfiguration(hostUrl); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", @@ -22,6 +23,7 @@ static void Main(string[] args) server.OpenAsync().Wait(); + Console.WriteLine($"WebAPI Hosted and listening on: {hostUrl}"); Console.ReadKey(); server.CloseAsync().Wait(); diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index c919a09..2e45146 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -60,7 +60,7 @@ public int SharedTimeSpan get // required for property visibility { if (!_sharedTimeSpan.HasValue) - throw new Exception("should not be called without value set"); + throw new Exception("should not be called without value set"); return _sharedTimeSpan.Value; } set { _sharedTimeSpan = value; } @@ -80,7 +80,7 @@ public int SharedTimeSpan /// Class used to generate caching keys /// public Type CacheKeyGenerator { get; set; } - + // cache repository private IApiOutputCache _webApiCache; @@ -106,7 +106,7 @@ protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool an return false; } - return actionContext.Request.Method == HttpMethod.Get; + return actionContext.Request.Method == HttpMethod.Get; } protected virtual void EnsureCacheTimeQuery() @@ -156,12 +156,18 @@ protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration co return responseMediaType; } + private bool IsNoCacheHeaderInRequest(HttpActionContext actionContext) + { + var cacheControl = actionContext.Request.Headers.CacheControl; + return cacheControl != null && cacheControl.NoCache; + } + public override void OnActionExecuting(HttpActionContext actionContext) { if (actionContext == null) throw new ArgumentNullException("actionContext"); if (!IsCachingAllowed(actionContext, AnonymousOnly)) return; - + var config = actionContext.Request.GetConfiguration(); EnsureCacheTimeQuery(); @@ -175,6 +181,8 @@ public override void OnActionExecuting(HttpActionContext actionContext) if (!_webApiCache.Contains(cachekey)) return; + if (IsNoCacheHeaderInRequest(actionContext)) return; + if (actionContext.Request.Headers.IfNoneMatch != null) { var etag = _webApiCache.Get(cachekey + Constants.EtagKey); @@ -208,8 +216,11 @@ public override void OnActionExecuting(HttpActionContext actionContext) } public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return; + { + if (actionExecutedContext.ActionContext.Response == null || + !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode || + IsNoCacheHeaderInRequest(actionExecutedContext.ActionContext)) + return; if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return; @@ -223,6 +234,11 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio var responseMediaType = actionExecutedContext.Request.Properties[CurrentRequestMediaType] as MediaTypeHeaderValue ?? GetExpectedMediaType(httpConfig, actionExecutedContext.ActionContext); var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, responseMediaType, ExcludeQueryStringFromCacheKey); + //if (IsNoCacheHeaderInRequest(actionExecutedContext.ActionContext)) + //{ + // _webApiCache.Remove(cachekey); + //} + if (!string.IsNullOrWhiteSpace(cachekey) && !(_webApiCache.Contains(cachekey))) { SetEtag(actionExecutedContext.Response, CreateEtag(actionExecutedContext, cachekey, cacheTime)); @@ -242,12 +258,12 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio _webApiCache.Add(baseKey, string.Empty, cacheTime.AbsoluteExpiration); _webApiCache.Add(cachekey, content, cacheTime.AbsoluteExpiration, baseKey); - + _webApiCache.Add(cachekey + Constants.ContentTypeKey, contentType, cacheTime.AbsoluteExpiration, baseKey); - + _webApiCache.Add(cachekey + Constants.EtagKey, etag, cacheTime.AbsoluteExpiration, baseKey); @@ -263,12 +279,12 @@ protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private) { var cachecontrol = new CacheControlHeaderValue - { - MaxAge = cacheTime.ClientTimeSpan, - SharedMaxAge = cacheTime.SharedTimeSpan, - MustRevalidate = MustRevalidate, - Private = Private - }; + { + MaxAge = cacheTime.ClientTimeSpan, + SharedMaxAge = cacheTime.SharedTimeSpan, + MustRevalidate = MustRevalidate, + Private = Private + }; response.Headers.CacheControl = cachecontrol; } @@ -293,4 +309,4 @@ private static void SetEtag(HttpResponseMessage message, string etag) } } } -} +} diff --git a/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs b/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs index e4b8cc0..f4dd44d 100644 --- a/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs +++ b/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs @@ -84,21 +84,21 @@ public void maxage_mustrevalidate_headers_correct_with_clienttimeout_zero_with_m } - [Test] - public void nocache_headers_correct() - { - var client = new HttpClient(_server); - var result = client.GetAsync(_url + "Get_nocache").Result; - - Assert.IsTrue(result.Headers.CacheControl.NoCache, - "NoCache in result headers was expected to be true when CacheOutput.NoCache=true."); - Assert.IsTrue(result.Headers.Contains("Pragma"), - "result headers does not contain expected Pragma."); - Assert.IsTrue(result.Headers.GetValues("Pragma").Contains("no-cache"), - "expected no-cache Pragma was not found"); - } - - [Test] + [Test] + public void nocache_headers_correct() + { + var client = new HttpClient(_server); + var result = client.GetAsync(_url + "Get_nocache").Result; + + Assert.IsTrue(result.Headers.CacheControl.NoCache, + "NoCache in result headers was expected to be true when CacheOutput.NoCache=true."); + Assert.IsTrue(result.Headers.Contains("Pragma"), + "result headers does not contain expected Pragma."); + Assert.IsTrue(result.Headers.GetValues("Pragma").Contains("no-cache"), + "expected no-cache Pragma was not found"); + } + + [Test] public void maxage_mustrevalidate_true_headers_correct() { var client = new HttpClient(_server); @@ -122,9 +122,9 @@ public void maxage_private_true_headers_correct() public void maxage_mustrevalidate_headers_correct_with_cacheuntil() { var client = new HttpClient(_server); - var result = client.GetAsync(_url + "Get_until25012015_1700").Result; - var clientTimeSpanSeconds = new SpecificTime(2017, 01, 25, 17, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds; - var resultCacheControlSeconds = ((TimeSpan) result.Headers.CacheControl.MaxAge).TotalSeconds; + var result = client.GetAsync(_url + "Get_until25012020_1700").Result; + var clientTimeSpanSeconds = new SpecificTime(2020, 01, 25, 17, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds; + var resultCacheControlSeconds = ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds; Assert.IsTrue(Math.Round(clientTimeSpanSeconds - resultCacheControlSeconds) == 0); Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); } @@ -135,7 +135,7 @@ public void maxage_mustrevalidate_headers_correct_with_cacheuntil_today() var client = new HttpClient(_server); var result = client.GetAsync(_url + "Get_until2355_today").Result; - Assert.IsTrue(Math.Round(new ThisDay(23,55,59).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); + Assert.IsTrue(Math.Round(new ThisDay(23, 55, 59).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); } @@ -145,7 +145,7 @@ public void maxage_mustrevalidate_headers_correct_with_cacheuntil_this_month() var client = new HttpClient(_server); var result = client.GetAsync(_url + "Get_until27_thismonth").Result; - Assert.IsTrue(Math.Round(new ThisMonth(27,0,0,0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); + Assert.IsTrue(Math.Round(new ThisMonth(27, 0, 0, 0).Execute(DateTime.Now).ClientTimeSpan.TotalSeconds - ((TimeSpan)result.Headers.CacheControl.MaxAge).TotalSeconds) == 0); Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); } @@ -183,7 +183,7 @@ public void shared_max_age_header_correct() { var client = new HttpClient(_server); var result = client.GetAsync(_url + "Get_c100_s100_sm200").Result; - Assert.AreEqual(result.Headers.CacheControl.SharedMaxAge,TimeSpan.FromSeconds(200)); + Assert.AreEqual(result.Headers.CacheControl.SharedMaxAge, TimeSpan.FromSeconds(200)); } [Test] @@ -194,6 +194,22 @@ public void shared_max_age_header_not_present() Assert.AreEqual(result.Headers.CacheControl.SharedMaxAge, null); } + [Test] + public void no_caching_headers_in_response_when_nocache_header_present_in_request_headers() + { + var client = new HttpClient(_server); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue + { + NoCache = true + }; + var result = client.GetAsync(_url + "Get_c100_s100").Result; + + Assert.IsNull(result.Headers.CacheControl, + "Cache-Control should not not be present in response when NoCache is present in request headers."); + Assert.IsNull(result.Headers.ETag, + "ETag should not not be present in response when NoCache is present in request headers."); + } + [TestFixtureTearDown] public void fixture_dispose() { diff --git a/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs b/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs index bdf66b8..608105e 100644 --- a/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs +++ b/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs @@ -53,12 +53,26 @@ public void set_cache_to_predefined_value() _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); } + [Test] + public void nocache_in_request_refreshes_cache() + { + var client = new HttpClient(_server); + client.DefaultRequestHeaders.CacheControl = + new CacheControlHeaderValue { NoCache = true }; + var result = client.GetAsync(_url + "Get_c100_s100").Result; + _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8")), Times.Exactly(2)); + _cache.Verify(s => s.Remove(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8")), Times.Exactly(1)); + _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), null), Times.Once()); + _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100:application/json; charset=utf-8"), It.IsAny(), It.Is(x => x <= DateTime.Now.AddSeconds(100)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s100")), Times.Once()); + } + + [Test] public void set_cache_to_predefined_value_c100_s0() { var client = new HttpClient(_server); var result = client.GetAsync(_url + "Get_c100_s0").Result; - + // NOTE: Should we expect the _cache to not be called at all if the ServerTimeSpan is 0? _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-get_c100_s0:application/json; charset=utf-8")), Times.Once()); // NOTE: Server timespan is 0, so there should not have been any Add at all. diff --git a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs index 645ad07..c5e1ad9 100644 --- a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs +++ b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs @@ -1,6 +1,8 @@ using System.Net; using System.Net.Http; using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; using WebApi.OutputCache.V2.TimeAttributes; namespace WebApi.OutputCache.V2.Tests.TestControllers @@ -25,17 +27,17 @@ public string Get_c0_s100() return "test"; } - [CacheOutput(NoCache=true)] + [CacheOutput(NoCache = true)] public string Get_nocache() { return "test"; } - [CacheOutput(ClientTimeSpan = 0, ServerTimeSpan = 100, MustRevalidate = true)] - public string Get_c0_s100_mustR() - { - return "test"; - } + [CacheOutput(ClientTimeSpan = 0, ServerTimeSpan = 100, MustRevalidate = true)] + public string Get_c0_s100_mustR() + { + return "test"; + } [CacheOutput(ClientTimeSpan = 50, MustRevalidate = true)] public string Get_c50_mustR() @@ -64,7 +66,7 @@ public string Get_s50_exclude_fakecallback(int? id = null, string callback = nul [CacheOutput(ServerTimeSpan = 50, ExcludeQueryStringFromCacheKey = false)] public string Get_s50_exclude_false(int id) { - return "test"+id; + return "test" + id; } [CacheOutput(ServerTimeSpan = 50, ExcludeQueryStringFromCacheKey = true)] @@ -73,13 +75,13 @@ public string Get_s50_exclude_true(int id) return "test" + id; } - [CacheOutputUntil(2017,01,25,17,00)] - public string Get_until25012015_1700() + [CacheOutputUntil(2020, 01, 25, 17, 00)] + public string Get_until25012020_1700() { return "test"; } - [CacheOutputUntilToday(23,55)] + [CacheOutputUntilToday(23, 55)] public string Get_until2355_today() { return "value"; @@ -91,7 +93,7 @@ public string Get_until27_thismonth() return "value"; } - [CacheOutputUntilThisYear(7,31)] + [CacheOutputUntilThisYear(7, 31)] public string Get_until731_thisyear() { return "value"; @@ -125,7 +127,7 @@ public string Get_request_exception_noCache() [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] public string Get_request_httpResponseException_noCache() { - throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Conflict){ReasonPhrase = "Fault shouldn't cache"}); + throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Conflict) { ReasonPhrase = "Fault shouldn't cache" }); } [CacheOutput(ClientTimeSpan = 50, ServerTimeSpan = 50)] @@ -160,4 +162,4 @@ public string Get_c100_s100_sm200() } } -} +} \ No newline at end of file diff --git a/test/WebApi.OutputCache.V2.Tests/WebApi.OutputCache.V2.Tests.csproj b/test/WebApi.OutputCache.V2.Tests/WebApi.OutputCache.V2.Tests.csproj index 2c8d426..fba230f 100644 --- a/test/WebApi.OutputCache.V2.Tests/WebApi.OutputCache.V2.Tests.csproj +++ b/test/WebApi.OutputCache.V2.Tests/WebApi.OutputCache.V2.Tests.csproj @@ -50,9 +50,9 @@ ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll True - - False - ..\..\packages\NUnit.2.6.2\lib\nunit.framework.dll + + ..\..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll + True @@ -116,6 +116,9 @@ WebApi2.OutputCache + + +