Skip to content

Commit cdd8923

Browse files
committed
WIP: First compiling implementation (untested) of support for self expiring cache results -- that control & construct their own Cache item policy from the cache item factory logic and data only available at runtime from the results within the execution of the cache item factory.
1 parent 6141102 commit cdd8923

File tree

7 files changed

+252
-48
lines changed

7 files changed

+252
-48
lines changed

LazyCacheHelpers/CacheHelpers/LazyCachePolicy.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static CacheItemPolicy NewAbsoluteExpirationPolicy(TimeSpan cacheTimeSpan
6666
private static readonly ThreadLocal<Random> _threadLocalRandomGenerator = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));
6767

6868
/// <summary>
69-
/// Provides logic to randomly distribute Cache TTL values within the specified threshhold to help ensure that
69+
/// Provides logic to randomly distribute Cache TTL values within the specified threshold to help ensure that
7070
/// cached values are not all exactly equal. This is helpful to provide dynamic caching that is much less likely
7171
/// to result in all elements being expired from cache at the exact same time, but balances this with the fact that
7272
/// they should expire relatively close in time. So the Threshold value should be small; e.g. with 30 seconds, or
@@ -84,7 +84,7 @@ public static TimeSpan RandomizeCacheTTLDistribution(TimeSpan timeSpanTTL, int m
8484
//BBernard
8585
//Salt the timespan with a small randomly generated offset value (e.g. spice it up a little with some salt)!
8686
//NOTE: We use a random number generator to help us salt our Cache intervals and better
87-
// stimilate offset caching of items. This is a simplistic way to help level out the
87+
// stimulate offset caching of items. This is a simplistic way to help level out the
8888
// cache hit/miss distribution so that fewer requests take the same large hit to refresh
8989
// when they expire by absolute time.
9090
//For Example: If in a high load environment multiple requests could all result in cache misses
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Runtime.Caching;
3+
using System.Threading.Tasks;
4+
5+
namespace LazyCacheHelpers
6+
{
7+
/// <summary>
8+
/// BBernard
9+
/// Original Source (MIT License): https://github.com/cajuncoding/LazyCacheHelpers
10+
///
11+
/// Public Interface for the Lazy Cache Handler which adds support for the value factories to be self-managing/self-expiring
12+
/// by returning both the result and the policy! This is very useful in use cases such as loading Auth Tokens or other
13+
/// data from external APIs whereby the API response determines how long the result is valid for, and therefore it does
14+
/// not need to be refreshed/reloaded until that time has lapsed. So the logic can build a Cache Expiration policy based
15+
/// on the information in the response and create a valid CachePolicy.
16+
///
17+
/// NOTE: This is intended to be an extension of the base ILazyCacheHandler, but is separate now as a composable interface to
18+
/// to mitigate potentially breaking existing custom implementations of the ILazyCacheHandler.
19+
/// </summary>
20+
/// <typeparam name="TValue"></typeparam>
21+
public interface ILazyCacheHandlerSelfExpiringResults<TValue>
22+
{
23+
TValue GetOrAddFromCache<TKey>(TKey key, Func<ILazySelfExpiringCacheResult<TValue>> fnValueFactory);
24+
Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Task<ILazySelfExpiringCacheResult<TValue>>> fnAsyncValueFactory);
25+
}
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Runtime.Caching;
2+
3+
namespace LazyCacheHelpers
4+
{
5+
public interface ILazySelfExpiringCacheResult<out TValue>
6+
{
7+
CacheItemPolicy CachePolicy { get; }
8+
TValue CacheItem { get; }
9+
}
10+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Runtime.Caching;
3+
4+
namespace LazyCacheHelpers
5+
{
6+
public class LazySelfExpiringCacheResult<TValue> : ILazySelfExpiringCacheResult<TValue>
7+
{
8+
public LazySelfExpiringCacheResult(TValue cacheItem, ILazyCachePolicy cachePolicy)
9+
: this(cacheItem, cachePolicy?.GeneratePolicy())
10+
{ }
11+
12+
public LazySelfExpiringCacheResult(TValue cacheItem, CacheItemPolicy cachePolicy)
13+
{
14+
CacheItem = cacheItem;
15+
CachePolicy = cachePolicy ?? throw new ArgumentNullException(nameof(cachePolicy));
16+
}
17+
18+
public CacheItemPolicy CachePolicy { get; }
19+
20+
public TValue CacheItem { get; }
21+
22+
public static LazySelfExpiringCacheResult<TValue> NewAbsoluteExpirationResult(TValue cacheItem, int absoluteExpirationMillis)
23+
=> NewAbsoluteExpirationResult(cacheItem, TimeSpan.FromMilliseconds(absoluteExpirationMillis));
24+
25+
public static LazySelfExpiringCacheResult<TValue> NewAbsoluteExpirationResult(TValue cacheItem, TimeSpan absoluteExpirationTimeSpan)
26+
=> new LazySelfExpiringCacheResult<TValue>(cacheItem, LazyCachePolicy.NewAbsoluteExpirationPolicy(absoluteExpirationTimeSpan));
27+
}
28+
}

LazyCacheHelpers/DefaultLazyCache.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,29 @@ public static TValue GetOrAddFromCache<TKey, TValue>(TKey key, Func<TValue> fnVa
5656
where TValue : class
5757
{
5858
TValue result = LazyCachePolicy.IsPolicyEnabled(cacheItemPolicy)
59-
? (TValue) _lazyCache.GetOrAddFromCache(key, fnValueFactory, cacheItemPolicy)
60-
: fnValueFactory();
59+
? (TValue) _lazyCache.GetOrAddFromCache(key, fnValueFactory, cacheItemPolicy)
60+
: fnValueFactory();
61+
return result;
62+
}
63+
64+
/// <summary>
65+
/// Add or update the cache with the specified cache key and item that will be Lazy Initialized from Lambda function/logic.
66+
/// In this overload the logic must also construct and return the result as well as the cache expiration policy together
67+
/// as any implementation of ILazySelfExpiringCacheResult&lt;TValue&gt; of which a default implementation can be easily
68+
/// created from LazySelfExpiringCacheResult&lt;TValue&gt;.NewAbsoluteExpirationResult(...).
69+
///
70+
/// This method ensures that the item is initialized with full thread safety and that only one thread ever executes the work
71+
/// to initialize the item to be cached (Self-populated Cache) -- significantly improving server utilization and performance.
72+
/// </summary>
73+
/// <typeparam name="TKey"></typeparam>
74+
/// <typeparam name="TValue"></typeparam>
75+
/// <param name="key"></param>
76+
/// <param name="fnValueFactory"></param>
77+
/// <returns></returns>
78+
public static TValue GetOrAddFromCache<TKey, TValue>(TKey key, Func<ILazySelfExpiringCacheResult<TValue>> fnValueFactory)
79+
where TValue : class
80+
{
81+
var result = (TValue)_lazyCache.GetOrAddFromCache(key, fnValueFactory);
6182
return result;
6283
}
6384

@@ -98,9 +119,34 @@ public static async Task<TValue> GetOrAddFromCacheAsync<TKey, TValue>(TKey key,
98119
var wrappedFnValueFactory = new Func<Task<object>>(async () => await fnAsyncValueFactory());
99120

100121
TValue result = LazyCachePolicy.IsPolicyEnabled(cacheItemPolicy)
101-
? (TValue) await _lazyCache.GetOrAddFromCacheAsync(key, wrappedFnValueFactory, cacheItemPolicy)
102-
: await fnAsyncValueFactory();
122+
? (TValue)await _lazyCache.GetOrAddFromCacheAsync(key, wrappedFnValueFactory, cacheItemPolicy)
123+
: await fnAsyncValueFactory();
124+
125+
return result;
126+
}
127+
128+
/// <summary>
129+
/// Add or update the cache with the specified cache key and item that will be Lazy Initialized from Lambda function/logic.
130+
/// In this overload the logic must also construct and return the result as well as the cache expiration policy together
131+
/// as any implementation of ILazySelfExpiringCacheResult&lt;TValue&gt; of which a default implementation can be easily
132+
/// created from LazySelfExpiringCacheResult&lt;TValue&gt;.NewAbsoluteExpirationResult(...).
133+
///
134+
/// This method ensures that the item is initialized with full thread safety and that only one thread ever executes the work
135+
/// to initialize the item to be cached (Self-populated Cache) -- significantly improving server utilization and performance.
136+
/// </summary>
137+
/// <typeparam name="TKey"></typeparam>
138+
/// <typeparam name="TValue"></typeparam>
139+
/// <param name="key"></param>
140+
/// <param name="fnAsyncValueFactory"></param>
141+
/// <returns></returns>
142+
public static async Task<TValue> GetOrAddFromCacheAsync<TKey, TValue>(TKey key, Func<Task<ILazySelfExpiringCacheResult<TValue>>> fnAsyncValueFactory)
143+
where TValue : class
144+
{
145+
//Because the underlying cache is set up to store any object and the async coercion isn't as easy as the synchronous,
146+
// we must wrap the original generics typed async factory into a new Func<> that matches the required type.
147+
var wrappedFnValueFactory = new Func<Task<ILazySelfExpiringCacheResult<object>>>(async () => await fnAsyncValueFactory());
103148

149+
var result = (TValue)await _lazyCache.GetOrAddFromCacheAsync(key, wrappedFnValueFactory);
104150
return result;
105151
}
106152

LazyCacheHelpers/LazyCacheHandler.cs

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace LazyCacheHelpers
2323
/// NOTE: A wrapper implementation for MemoryCache is implemented (via default ICacheRepository implementation as LazyDotNetMemoryCacheRepository) to
2424
/// make working with MemoryCache with greatly simplified support for self-populating (Lazy) initialization.
2525
/// </summary>
26-
public class LazyCacheHandler<TValue> : ILazyCacheHandler<TValue> where TValue : class
26+
public class LazyCacheHandler<TValue> : ILazyCacheHandler<TValue>, ILazyCacheHandlerSelfExpiringResults<TValue> where TValue : class
2727
{
2828
//Added methods to CacheHelper to work with MemoryCache more easily.
2929
//NOTE: .Net MemoryCache supports this does NOT support Garbage Collection and Resource Reclaiming so it should
@@ -46,8 +46,14 @@ public LazyCacheHandler()
4646
{}
4747

4848
/// <summary>
49-
/// BBernard
50-
/// A wrapper implementation for ICacheRepository to make working with Thread Safety significantly easier.
49+
/// This overload enables dynamic self-populating cache retrieval of the results on any result generated by the specified
50+
/// cache item/result factory Func&lt;T&gt;. Using the Cache Item Expiration Policy provided the cache will ensure that
51+
/// the work is only ever performed by one and only one request, whereby all other simultaneous requests will immediately
52+
/// benefit from the work and receive the cached item once it's generated.
53+
///
54+
/// In this overload the CacheItem policy is known before executing the logic so it is consistent for all calls to this method.
55+
///
56+
/// Ultimately this is a wrapper implementation for ICacheRepository to make working with Thread Safety significantly easier.
5157
/// This provides completely ThreadSafe cache with Lazy Loading capabilities in an easy to use function;
5258
/// Lazy<typeparamref name="TValue"/> loading facilitates self-populating cache so that the long running processes are never
5359
/// executed more than once, even if they are triggered at approx. the same time.
@@ -59,11 +65,11 @@ public LazyCacheHandler()
5965
/// https://blog.falafel.com/working-system-runtime-caching-memorycache/
6066
/// </summary>
6167
/// <typeparam name="TKey"></typeparam>
62-
/// <typeparam name="T"></typeparam>
6368
/// <param name="key"></param>
6469
/// <param name="fnValueFactory"></param>
6570
/// <param name="cacheItemPolicy"></param>
6671
/// <returns></returns>
72+
/// <exception cref="ArgumentNullException"></exception>
6773
public virtual TValue GetOrAddFromCache<TKey>(TKey key, Func<TValue> fnValueFactory, CacheItemPolicy cacheItemPolicy)
6874
{
6975
//We support either ILazyCacheKey interface or any object for the Cache Key as long as it's ToString()
@@ -99,9 +105,54 @@ public virtual TValue GetOrAddFromCache<TKey>(TKey key, Func<TValue> fnValueFact
99105
}
100106

101107
/// <summary>
102-
/// BBernard
108+
/// This overload enables dynamic self-populating cache retrieval whereby the actual cache item logic also returns
109+
/// the cache expiration policy in addition to the cache item result. This is very useful in cases such as Auth tokens,
110+
/// and external API calls whereby the response contains information about how long the data returned is valid. And therefore
111+
/// the response can be used to construct a highly optimized Cache Expiration Policy based on the data returned -- rather than
112+
/// simply guessing and/or hard coding how long the data is valid for.
113+
/// </summary>
114+
/// <typeparam name="TKey"></typeparam>
115+
/// <param name="key"></param>
116+
/// <param name="fnValueFactory"></param>
117+
/// <returns></returns>
118+
/// <exception cref="ArgumentNullException"></exception>
119+
public TValue GetOrAddFromCache<TKey>(TKey key, Func<ILazySelfExpiringCacheResult<TValue>> fnValueFactory)
120+
{
121+
if (fnValueFactory == null)
122+
throw new ArgumentNullException(nameof(fnValueFactory));
123+
124+
//TODO: WIP: TEST THAT this REF Approach works...
125+
CacheItemPolicy cacheItemPolicyFromResultRef = null;
126+
return GetOrAddFromCache(key, () =>
127+
{
128+
//Execute the original Cache Factory method...
129+
var selfExpiringCacheResult = fnValueFactory.Invoke();
130+
//Now unwrap the results and set our CacheItemPolicy Reference from the result into our captured ref...
131+
cacheItemPolicyFromResultRef = selfExpiringCacheResult.CachePolicy;
132+
133+
//Validate that the returned policy is valid; otherwise we used a Disabled Cache Policy fallback to ensure
134+
// that the code still runs without issue...
135+
if (!LazyCachePolicy.IsPolicyEnabled(cacheItemPolicyFromResultRef))
136+
cacheItemPolicyFromResultRef = LazyCachePolicy.DisabledCachingPolicy;
137+
138+
//Finally return the actual result just as a normal cache item factory would...
139+
return selfExpiringCacheResult.CacheItem;
140+
},
141+
//Here we pass in our Ref that will be dynamically updated by the cache item factory if it executes,
142+
// otherwise this will not be used since the value is loaded from the cache...
143+
cacheItemPolicyFromResultRef
144+
);
145+
}
146+
147+
/// <summary>
148+
/// This overload enables async dynamic self-populating cache retrieval of the results on any result generated by the specified
149+
/// cache item/result factory Func&lt;T&gt;. Using the Cache Item Expiration Policy provided the cache will ensure that
150+
/// the work is only ever performed by one and only one request, whereby all other simultaneous requests will immediately
151+
/// benefit from the work and receive the cached item once it's generated.
103152
///
104-
/// An Async wrapper implementation for using ICacheRepository to make working with Thread Safety for
153+
/// In this overload the CacheItem policy is known before executing the logic so it is consistent for all calls to this method.
154+
///
155+
/// Ultimately this is an Async wrapper implementation for using ICacheRepository to make working with Thread Safety for
105156
/// Asynchronous processes significantly easier. This provides completely ThreadSafe Async cache with Lazy Loading capabilities
106157
/// in an easy to use function; Lazy<typeparamref name="TValue"/> loading facilitates self-populating cache so that the long running processes are never
107158
/// executed more than once, even if they are triggered at approx. the same time.
@@ -123,14 +174,18 @@ public virtual TValue GetOrAddFromCache<TKey>(TKey key, Func<TValue> fnValueFact
123174
/// the Async/Await Task Based Asynchronous pattern from top to bottom and even with our caching!
124175
///
125176
/// </summary>
126-
/// <typeparam name="T"></typeparam>
177+
/// <typeparam name="TKey"></typeparam>
127178
/// <param name="key"></param>
128179
/// <param name="fnAsyncValueFactory"></param>
129180
/// <param name="cacheItemPolicy"></param>
130181
/// <returns></returns>
182+
/// <exception cref="ArgumentNullException"></exception>
131183
public virtual async Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Task<TValue>> fnAsyncValueFactory, CacheItemPolicy cacheItemPolicy)
132184
{
133-
//We support eitehr ILazyCacheKey interface or any object for the Cache Key as long as it's ToString()
185+
if (fnAsyncValueFactory == null)
186+
throw new ArgumentNullException(nameof(fnAsyncValueFactory));
187+
188+
//We support either ILazyCacheKey interface or any object for the Cache Key as long as it's ToString()
134189
// implementation creates a valid unique Key for us, so here we initialize the Cache Key to use.
135190
string cacheKey = GenerateCacheKeyHelper(key);
136191

@@ -179,6 +234,46 @@ public virtual async Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Ta
179234
}
180235
}
181236

237+
/// <summary>
238+
/// This overload enables async dynamic self-populating cache retrieval whereby the actual cache item logic also returns
239+
/// the cache expiration policy in addition to the cache item result. This is very useful in cases such as Auth tokens,
240+
/// and external API calls whereby the response contains information about how long the data returned is valid. And therefore
241+
/// the response can be used to construct a highly optimized Cache Expiration Policy based on the data returned -- rather than
242+
/// simply guessing and/or hard coding how long the data is valid for.
243+
/// </summary>
244+
/// <typeparam name="TKey"></typeparam>
245+
/// <param name="key"></param>
246+
/// <param name="fnAsyncValueFactory"></param>
247+
/// <returns></returns>
248+
/// <exception cref="ArgumentNullException"></exception>
249+
public Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Task<ILazySelfExpiringCacheResult<TValue>>> fnAsyncValueFactory)
250+
{
251+
if (fnAsyncValueFactory == null)
252+
throw new ArgumentNullException(nameof(fnAsyncValueFactory));
253+
254+
//TODO: WIP: TEST THAT this REF Approach works...
255+
CacheItemPolicy cacheItemPolicyFromResultRef = null;
256+
return GetOrAddFromCacheAsync(key, async () =>
257+
{
258+
//Execute the original Cache Factory method...
259+
var selfExpiringCacheResult = await fnAsyncValueFactory.Invoke();
260+
//Now unwrap the results and set our CacheItemPolicy Reference from the result into our captured ref...
261+
cacheItemPolicyFromResultRef = selfExpiringCacheResult.CachePolicy;
262+
263+
//Validate that the returned policy is valid; otherwise we used a Disabled Cache Policy fallback to ensure
264+
// that the code still runs without issue...
265+
if (!LazyCachePolicy.IsPolicyEnabled(cacheItemPolicyFromResultRef))
266+
cacheItemPolicyFromResultRef = LazyCachePolicy.DisabledCachingPolicy;
267+
268+
//Finally return the actual result just as a normal cache item factory would...
269+
return selfExpiringCacheResult.CacheItem;
270+
},
271+
//Here we pass in our Ref that will be dynamically updated by the cache item factory if it executes,
272+
// otherwise this will not be used since the value is loaded from the cache...
273+
cacheItemPolicyFromResultRef
274+
);
275+
}
276+
182277
/// <summary>
183278
/// BBernard
184279
///
@@ -232,6 +327,5 @@ protected virtual string GenerateCacheKeyHelper<TKey>(TKey cacheKeyGenerator)
232327
}
233328

234329
#endregion
235-
236330
}
237331
}

0 commit comments

Comments
 (0)