From 0388c694bd7b34b08648a9f691db902519f8a701 Mon Sep 17 00:00:00 2001 From: "ondrej.netocny" Date: Tue, 1 Dec 2020 21:46:31 +0100 Subject: [PATCH 1/5] Added PreciseTime class to be able to specify precise caching timeouts using TimeSpan. Added new properties to CacheOutputAttribute to be able to specify timeouts by TimeSpan and used PreciseTime. --- .gitignore | 1 + .../Time/PreciseTime.cs | 28 ++++++++++ .../WebApi.OutputCache.Core.csproj | 1 + .../CacheOutputAttribute.cs | 51 ++++++++++++++----- 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 src/WebApi.OutputCache.Core/Time/PreciseTime.cs 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..2023f8a --- /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..0963541 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -5,8 +5,6 @@ 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; @@ -43,16 +41,21 @@ public class CacheOutputAttribute : ActionFilterAttribute /// /// How long response should be cached on the server side (in seconds) /// - public int ServerTimeSpan { get; set; } + public int ServerTimeSpan + { + get => (int) _serverTimeout.TotalSeconds; + set => _serverTimeout = TimeSpan.FromSeconds(value); + } /// /// Corresponds to CacheControl MaxAge HTTP header (in seconds) /// - public int ClientTimeSpan { get; set; } - + public int ClientTimeSpan + { + get => (int)_clientTimeout.TotalSeconds; + set => _clientTimeout = TimeSpan.FromSeconds(value); + } - private int? _sharedTimeSpan = null; - /// /// Corresponds to CacheControl Shared MaxAge HTTP header (in seconds) /// @@ -60,11 +63,32 @@ public int SharedTimeSpan { get // required for property visibility { - if (!_sharedTimeSpan.HasValue) - throw new Exception("should not be called without value set"); - return _sharedTimeSpan.Value; + if (!_sharedTimeout.HasValue) + { + throw new Exception("should not be called without value set"); + } + return (int) _sharedTimeout.Value.TotalSeconds; } - set { _sharedTimeSpan = value; } + set => _sharedTimeout = TimeSpan.FromSeconds(value); + } + + + public TimeSpan ClientTimeout + { + get => _clientTimeout; + set => _clientTimeout = value; + } + + public TimeSpan ServerTimeout + { + get => _serverTimeout; + set => _serverTimeout = value; + } + + public TimeSpan? SharedTimeout + { + get => _sharedTimeout; + set => _sharedTimeout = value; } /// @@ -101,6 +125,9 @@ protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage } internal IModelQuery CacheTimeQuery; + private TimeSpan _clientTimeout; + private TimeSpan _serverTimeout; + private TimeSpan? _sharedTimeout; protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) { @@ -127,7 +154,7 @@ protected virtual void EnsureCacheTimeQuery() protected void ResetCacheTimeQuery() { - CacheTimeQuery = new ShortTime( ServerTimeSpan, ClientTimeSpan, _sharedTimeSpan); + CacheTimeQuery = new PreciseTime(_serverTimeout, _clientTimeout, _sharedTimeout); } protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext) From be2da5dfc9278182192f5c8bccf2143d4add2bdc Mon Sep 17 00:00:00 2001 From: "ondrej.netocny" Date: Wed, 2 Dec 2020 08:46:19 +0100 Subject: [PATCH 2/5] Added comments to new properties. --- src/WebApi.OutputCache.V2/CacheOutputAttribute.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index 0963541..1323b74 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -72,19 +72,27 @@ public int SharedTimeSpan set => _sharedTimeout = TimeSpan.FromSeconds(value); } - + /// + /// Corresponds to CacheControl MaxAge HTTP header + /// public TimeSpan ClientTimeout { get => _clientTimeout; set => _clientTimeout = value; } + /// + /// How long response should be cached on the server side + /// public TimeSpan ServerTimeout { get => _serverTimeout; set => _serverTimeout = value; } + /// + /// Corresponds to CacheControl Shared MaxAge HTTP header + /// public TimeSpan? SharedTimeout { get => _sharedTimeout; From 150d9504fb46fceb589f29914732c03ad8de56a5 Mon Sep 17 00:00:00 2001 From: netaques Date: Wed, 2 Dec 2020 09:09:48 +0100 Subject: [PATCH 3/5] Fixed formatting. --- src/WebApi.OutputCache.Core/Time/PreciseTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebApi.OutputCache.Core/Time/PreciseTime.cs b/src/WebApi.OutputCache.Core/Time/PreciseTime.cs index 2023f8a..a63b520 100644 --- a/src/WebApi.OutputCache.Core/Time/PreciseTime.cs +++ b/src/WebApi.OutputCache.Core/Time/PreciseTime.cs @@ -17,7 +17,7 @@ public PreciseTime(TimeSpan server, TimeSpan client, TimeSpan? shared = null) public CacheTime Execute(DateTime model) { - return new CacheTime + return new CacheTime { AbsoluteExpiration = model.Add(_server), ClientTimeSpan = _client, From 0a1899578a1ae5bff9b62029e8fe7637c5a7fe2e Mon Sep 17 00:00:00 2001 From: netaques Date: Wed, 2 Dec 2020 11:22:30 +0100 Subject: [PATCH 4/5] Changed timeout property types from TimeSpan to string since TimeSpan cannot be passed as attribute property. Added client and server side tests. --- .../CacheOutputAttribute.cs | 34 ++++++++++++------- .../ClientSideTests.cs | 10 ++++++ .../ServerSideTests.cs | 12 +++++++ .../TestControllers/SampleController.cs | 6 ++++ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index 1323b74..d1fad93 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; @@ -20,6 +21,8 @@ namespace WebApi.OutputCache.V2 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class CacheOutputAttribute : ActionFilterAttribute { + private static readonly IFormatProvider TimeoutFormatProvider = new CultureInfo("en-US"); + private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType"; protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") {CharSet = Encoding.UTF8.HeaderName}; @@ -52,7 +55,7 @@ public int ServerTimeSpan /// public int ClientTimeSpan { - get => (int)_clientTimeout.TotalSeconds; + get => (int) _clientTimeout.TotalSeconds; set => _clientTimeout = TimeSpan.FromSeconds(value); } @@ -73,30 +76,30 @@ public int SharedTimeSpan } /// - /// Corresponds to CacheControl MaxAge HTTP header + /// Corresponds to CacheControl MaxAge HTTP header (string in TimeSpan's en-US format) /// - public TimeSpan ClientTimeout + public string ClientTimeout { - get => _clientTimeout; - set => _clientTimeout = value; + get => _clientTimeout.ToString(); + set => _clientTimeout = ParseTimeSpan(value); } /// - /// How long response should be cached on the server side + /// How long response should be cached on the server side (string in TimeSpan's en-US format) /// - public TimeSpan ServerTimeout + public string ServerTimeout { - get => _serverTimeout; - set => _serverTimeout = value; + get => _serverTimeout.ToString(); + set => _serverTimeout = ParseTimeSpan(value); } /// - /// Corresponds to CacheControl Shared MaxAge HTTP header + /// Corresponds to CacheControl Shared MaxAge HTTP header (string in TimeSpan's en-US format) /// - public TimeSpan? SharedTimeout + public string SharedTimeout { - get => _sharedTimeout; - set => _sharedTimeout = value; + get => _sharedTimeout.ToString(); + set => _sharedTimeout = string.IsNullOrEmpty(value) ? null : (TimeSpan?) ParseTimeSpan(value); } /// @@ -407,5 +410,10 @@ private static void SetEtag(HttpResponseMessage message, string etag) message.Headers.ETag = eTag; } } + + private static TimeSpan ParseTimeSpan(string input) + { + return TimeSpan.Parse(input, TimeoutFormatProvider); + } } } 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..0f05c8b 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(ClientTimeout = "0:00:00:00.500", ServerTimeout = "0:00:00:00.500")] + public string GetStringTimeout_c500ms_s500ms() + { + return "test"; + } + [CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 0)] public string Get_c100_s0() { From 46682cb3339201e9c843ab95e94263c70234392b Mon Sep 17 00:00:00 2001 From: "ondrej.netocny" Date: Tue, 8 Dec 2020 10:15:27 +0100 Subject: [PATCH 5/5] Changed new properties into nullable ulong to handle precedence over existing properties. --- .../CacheOutputAttribute.cs | 102 +++++++++++------- .../TestControllers/SampleController.cs | 2 +- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index d1fad93..b0c714f 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -21,8 +21,6 @@ namespace WebApi.OutputCache.V2 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class CacheOutputAttribute : ActionFilterAttribute { - private static readonly IFormatProvider TimeoutFormatProvider = new CultureInfo("en-US"); - private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType"; protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") {CharSet = Encoding.UTF8.HeaderName}; @@ -42,64 +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 => (int) _serverTimeout.TotalSeconds; - set => _serverTimeout = TimeSpan.FromSeconds(value); - } + 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 => (int) _clientTimeout.TotalSeconds; - set => _clientTimeout = TimeSpan.FromSeconds(value); - } - + 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 (!_sharedTimeout.HasValue) + if (!_sharedTimeSpan.HasValue) { throw new Exception("should not be called without value set"); } - return (int) _sharedTimeout.Value.TotalSeconds; + return _sharedTimeSpan.Value; } - set => _sharedTimeout = TimeSpan.FromSeconds(value); + set => _sharedTimeSpan = value; } /// - /// Corresponds to CacheControl MaxAge HTTP header (string in TimeSpan's en-US format) + /// Corresponds to CacheControl MaxAge HTTP header in milliseconds. This property has precedence over ClientTimeSpan. If not set ClientTimeSpan is used. /// - public string ClientTimeout + public ulong ClientTimeSpanMillis { - get => _clientTimeout.ToString(); - set => _clientTimeout = ParseTimeSpan(value); + 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 (string in TimeSpan's en-US format) + /// 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 string ServerTimeout + public ulong ServerTimeSpanMillis { - get => _serverTimeout.ToString(); - set => _serverTimeout = ParseTimeSpan(value); + 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) + /// 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 string SharedTimeout + public ulong SharedTimeSpanMillis { - get => _sharedTimeout.ToString(); - set => _sharedTimeout = string.IsNullOrEmpty(value) ? null : (TimeSpan?) ParseTimeSpan(value); + get // required for property visibility + { + if (!_sharedTimeSpanMillis.HasValue) + { + throw new Exception("should not be called without value set"); + } + return _sharedTimeSpanMillis.Value; + } + set => _sharedTimeSpanMillis = value; } /// @@ -136,9 +149,10 @@ protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage } internal IModelQuery CacheTimeQuery; - private TimeSpan _clientTimeout; - private TimeSpan _serverTimeout; - private TimeSpan? _sharedTimeout; + + private ulong? _clientTimeSpanMillis; + private ulong? _serverTimeSpanMillis; + private ulong? _sharedTimeSpanMillis; protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) { @@ -165,7 +179,21 @@ protected virtual void EnsureCacheTimeQuery() protected void ResetCacheTimeQuery() { - CacheTimeQuery = new PreciseTime(_serverTimeout, _clientTimeout, _sharedTimeout); + 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) @@ -411,9 +439,9 @@ private static void SetEtag(HttpResponseMessage message, string etag) } } - private static TimeSpan ParseTimeSpan(string input) + private static TimeSpan CreateTimeSpan(ulong? millis, int seconds) { - return TimeSpan.Parse(input, TimeoutFormatProvider); + return millis.HasValue ? TimeSpan.FromMilliseconds(millis.Value) : TimeSpan.FromSeconds(seconds); } } } diff --git a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs index 0f05c8b..8c10636 100644 --- a/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs +++ b/test/WebApi.OutputCache.V2.Tests/TestControllers/SampleController.cs @@ -15,7 +15,7 @@ public string Get_c100_s100() return "test"; } - [CacheOutput(ClientTimeout = "0:00:00:00.500", ServerTimeout = "0:00:00:00.500")] + [CacheOutput(ClientTimeSpanMillis = 500, ServerTimeSpanMillis = 500)] public string GetStringTimeout_c500ms_s500ms() { return "test";