diff --git a/.gitignore b/.gitignore index bdc3535..afc0a14 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ Generated_Code #added for RIA/Silverlight projects _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML +/.vs diff --git a/src/WebApi.OutputCache.Core/Time/PreciseTime.cs b/src/WebApi.OutputCache.Core/Time/PreciseTime.cs new file mode 100644 index 0000000..a63b520 --- /dev/null +++ b/src/WebApi.OutputCache.Core/Time/PreciseTime.cs @@ -0,0 +1,28 @@ +using System; + +namespace WebApi.OutputCache.Core.Time +{ + public class PreciseTime : IModelQuery + { + private readonly TimeSpan _server; + private readonly TimeSpan _client; + private readonly TimeSpan? _shared; + + public PreciseTime(TimeSpan server, TimeSpan client, TimeSpan? shared = null) + { + _server = server; + _client = client; + _shared = shared; + } + + public CacheTime Execute(DateTime model) + { + return new CacheTime + { + AbsoluteExpiration = model.Add(_server), + ClientTimeSpan = _client, + SharedTimeSpan = _shared + }; + } + } +} \ No newline at end of file diff --git a/src/WebApi.OutputCache.Core/WebApi.OutputCache.Core.csproj b/src/WebApi.OutputCache.Core/WebApi.OutputCache.Core.csproj index 9d0d5d3..8e2575c 100644 --- a/src/WebApi.OutputCache.Core/WebApi.OutputCache.Core.csproj +++ b/src/WebApi.OutputCache.Core/WebApi.OutputCache.Core.csproj @@ -48,6 +48,7 @@ + diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index fc79e71..b0c714f 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; -using System.Runtime.ExceptionServices; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -41,30 +40,79 @@ public class CacheOutputAttribute : ActionFilterAttribute public bool ExcludeQueryStringFromCacheKey { get; set; } /// - /// How long response should be cached on the server side (in seconds) + /// How long response should be cached on the server side in seconds. ServerTimeSpanMillis has precedence over this property. /// public int ServerTimeSpan { get; set; } /// - /// Corresponds to CacheControl MaxAge HTTP header (in seconds) + /// Corresponds to CacheControl MaxAge HTTP header in seconds. ClientTimeSpanMillis has precedence over this property. /// public int ClientTimeSpan { get; set; } - private int? _sharedTimeSpan = null; /// - /// Corresponds to CacheControl Shared MaxAge HTTP header (in seconds) + /// Corresponds to CacheControl Shared MaxAge HTTP header in seconds. SharedTimeSpanMillis has precedence over this property. /// 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; } + set => _sharedTimeSpan = value; + } + + /// + /// Corresponds to CacheControl MaxAge HTTP header in milliseconds. This property has precedence over ClientTimeSpan. If not set ClientTimeSpan is used. + /// + public ulong ClientTimeSpanMillis + { + get // required for property visibility + { + if (!_clientTimeSpanMillis.HasValue) + { + throw new Exception("should not be called without value set"); + } + return _clientTimeSpanMillis.Value; + } + set => _clientTimeSpanMillis = value; + } + + /// + /// How long response should be cached on the server side in milliseconds. This property has precedence over ServerTimeSpan. If not set ServerTimeSpan is used. + /// + public ulong ServerTimeSpanMillis + { + get // required for property visibility + { + if (!_serverTimeSpanMillis.HasValue) + { + throw new Exception("should not be called without value set"); + } + return _serverTimeSpanMillis.Value; + } + set => _serverTimeSpanMillis = value; + } + + /// + /// Corresponds to CacheControl Shared MaxAge HTTP header (string in TimeSpan's en-US format) in milliseconds. This property has precedence over SharedTimeSpan. If not set SharedTimeSpan is used. + /// + public ulong SharedTimeSpanMillis + { + get // required for property visibility + { + if (!_sharedTimeSpanMillis.HasValue) + { + throw new Exception("should not be called without value set"); + } + return _sharedTimeSpanMillis.Value; + } + set => _sharedTimeSpanMillis = value; } /// @@ -102,6 +150,10 @@ protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage internal IModelQuery CacheTimeQuery; + private ulong? _clientTimeSpanMillis; + private ulong? _serverTimeSpanMillis; + private ulong? _sharedTimeSpanMillis; + protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) { if (anonymousOnly) @@ -127,7 +179,21 @@ protected virtual void EnsureCacheTimeQuery() protected void ResetCacheTimeQuery() { - CacheTimeQuery = new ShortTime( ServerTimeSpan, ClientTimeSpan, _sharedTimeSpan); + var serverTimeout = CreateTimeSpan(_serverTimeSpanMillis, ServerTimeSpan); + var clientTimeout = CreateTimeSpan(_clientTimeSpanMillis, ClientTimeSpan); + + TimeSpan? sharedTimeout = null; + + if (_sharedTimeSpanMillis.HasValue) + { + sharedTimeout = TimeSpan.FromMilliseconds(_sharedTimeSpanMillis.Value); + } + else if(_sharedTimeSpan.HasValue) + { + sharedTimeout = TimeSpan.FromSeconds(_sharedTimeSpan.Value); + } + + CacheTimeQuery = new PreciseTime(serverTimeout, clientTimeout, sharedTimeout); } protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext) @@ -372,5 +438,10 @@ private static void SetEtag(HttpResponseMessage message, string etag) message.Headers.ETag = eTag; } } + + private static TimeSpan CreateTimeSpan(ulong? millis, int seconds) + { + return millis.HasValue ? TimeSpan.FromMilliseconds(millis.Value) : TimeSpan.FromSeconds(seconds); + } } } diff --git a/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs b/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs index 1dc29e2..2c5175d 100644 --- a/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs +++ b/test/WebApi.OutputCache.V2.Tests/ClientSideTests.cs @@ -36,6 +36,16 @@ public void maxage_mustrevalidate_false_headers_correct() Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); } + [Test] + public void maxageinmillis_mustrevalidate_false_headers_correct() + { + var client = new HttpClient(_server); + var result = client.GetAsync(_url + "GetStringTimeout_c500ms_s500ms").Result; + + Assert.AreEqual(TimeSpan.FromMilliseconds(500), result.Headers.CacheControl.MaxAge); + Assert.IsFalse(result.Headers.CacheControl.MustRevalidate); + } + [Test] public void no_cachecontrol_when_clienttimeout_is_zero() { diff --git a/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs b/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs index 27912e7..58b70df 100644 --- a/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs +++ b/test/WebApi.OutputCache.V2.Tests/ServerSideTests.cs @@ -53,6 +53,18 @@ 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 set_cache_to_predefined_value_using_string_timeout() + { + var client = new HttpClient(_server); + var result = client.GetAsync(_url + "GetStringTimeout_c500ms_s500ms").Result; + + _cache.Verify(s => s.Contains(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-getstringtimeout_c500ms_s500ms:application/json; charset=utf-8")), Times.Exactly(2)); + _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-getstringtimeout_c500ms_s500ms"), It.IsAny(), It.Is(x => x < DateTime.Now.AddSeconds(1)), null), Times.Once()); + _cache.Verify(s => s.Add(It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-getstringtimeout_c500ms_s500ms:application/json; charset=utf-8"), It.IsAny(), It.Is(x => x < DateTime.Now.AddSeconds(1)), It.Is(x => x == "webapi.outputcache.v2.tests.testcontrollers.samplecontroller-getstringtimeout_c500ms_s500ms")), Times.Once()); + } + [Test] public void set_cache_to_predefined_value_c100_s0() { diff --git a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs index 9bbe49a..8c10636 100644 --- a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs +++ b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs @@ -15,6 +15,12 @@ public string Get_c100_s100() return "test"; } + [CacheOutput(ClientTimeSpanMillis = 500, ServerTimeSpanMillis = 500)] + public string GetStringTimeout_c500ms_s500ms() + { + return "test"; + } + [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 0)] public string Get_c100_s0() {