Skip to content

Commit a95f843

Browse files
committed
bug: if you change the type of the item in the cache the old item should get eveicted
fixes #46
1 parent 1c4b9db commit a95f843

File tree

2 files changed

+103
-22
lines changed

2 files changed

+103
-22
lines changed

LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,15 @@ public void GetOrAddAndThenGetObjectReturnsCorrectType()
252252
Assert.IsNotNull(actual);
253253
}
254254

255+
[Test]
256+
public void GetOrAddAndThenGetOrAddDifferentTypeDoesLastInWins()
257+
{
258+
var first = sut.GetOrAdd(TestKey, () => new object());
259+
var second = sut.GetOrAdd(TestKey, () => testObject);
260+
Assert.IsNotNull(second);
261+
Assert.IsInstanceOf<ComplexTestObject>(second);
262+
}
263+
255264
[Test]
256265
public void GetOrAddAndThenGetValueObjectReturnsCorrectType()
257266
{
@@ -268,6 +277,8 @@ public void GetOrAddAndThenGetWrongtypeObjectReturnsNull()
268277
Assert.IsNull(actual);
269278
}
270279

280+
281+
271282
[Test]
272283
public void GetOrAddAsyncACancelledTaskDoesNotCacheIt()
273284
{
@@ -315,6 +326,15 @@ public async Task GetOrAddAsyncAndThenGetAsyncObjectReturnsCorrectType()
315326
Assert.That(actual, Is.EqualTo(testObject));
316327
}
317328

329+
[Test]
330+
public async Task GetOrAddAsyncAndThenGetOrAddAsyncDifferentTypeDoesLastInWins()
331+
{
332+
var first = await sut.GetOrAddAsync(TestKey, () => Task.FromResult(new object()));
333+
var second = await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject));
334+
Assert.IsNotNull(second);
335+
Assert.IsInstanceOf<ComplexTestObject>(second);
336+
}
337+
318338
[Test]
319339
public async Task GetOrAddAsyncAndThenGetAsyncWrongObjectReturnsNull()
320340
{

LazyCache/CachingService.cs

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public virtual T Get<T>(string key)
7171

7272
var item = CacheProvider.Get(key);
7373

74-
return GetValueFromLazy<T>(item);
74+
return GetValueFromLazy<T>(item, out _);
7575
}
7676

7777
public virtual Task<T> GetAsync<T>(string key)
@@ -80,25 +80,27 @@ public virtual Task<T> GetAsync<T>(string key)
8080

8181
var item = CacheProvider.Get(key);
8282

83-
return GetValueFromAsyncLazy<T>(item);
83+
return GetValueFromAsyncLazy<T>(item, out _);
8484
}
8585

8686
public virtual T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory)
8787
{
8888
ValidateKey(key);
8989

9090
object cacheItem;
91+
92+
object CacheFactory(ICacheEntry entry) =>
93+
new Lazy<T>(() =>
94+
{
95+
var result = addItemFactory(entry);
96+
EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy<T>(entry.PostEvictionCallbacks);
97+
return result;
98+
});
99+
91100
locker.Wait(); //TODO: do we really need this? Could we just lock on the key?
92101
try
93102
{
94-
cacheItem = CacheProvider.GetOrCreate<object>(key, entry =>
95-
new Lazy<T>(() =>
96-
{
97-
var result = addItemFactory(entry);
98-
EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy<T>(entry.PostEvictionCallbacks);
99-
return result;
100-
})
101-
);
103+
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
102104
}
103105
finally
104106
{
@@ -107,7 +109,25 @@ public virtual T GetOrAdd<T>(string key, Func<ICacheEntry, T> addItemFactory)
107109

108110
try
109111
{
110-
return GetValueFromLazy<T>(cacheItem);
112+
var result = GetValueFromLazy<T>(cacheItem, out var valueHasChangedType);
113+
114+
// if we get a cache hit but for something with the wrong type we need to evict it, start again and cache the new item instead
115+
if (valueHasChangedType)
116+
{
117+
CacheProvider.Remove(key);
118+
locker.Wait(); //TODO: do we really need this? Could we just lock on the key?
119+
try
120+
{
121+
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
122+
}
123+
finally
124+
{
125+
locker.Release();
126+
}
127+
result = GetValueFromLazy<T>(cacheItem, out _ /* we just evicted so type change cannot happen this time */);
128+
}
129+
130+
return result;
111131
}
112132
catch //addItemFactory errored so do not cache the exception
113133
{
@@ -138,16 +158,18 @@ public virtual async Task<T> GetOrAddAsync<T>(string key, Func<ICacheEntry, Task
138158
await locker.WaitAsync()
139159
.ConfigureAwait(
140160
false); //TODO: do we really need to lock everything here - faster if we could lock on just the key?
161+
162+
object CacheFactory(ICacheEntry entry) =>
163+
new AsyncLazy<T>(() =>
164+
{
165+
var result = addItemFactory(entry);
166+
EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy<T>(entry.PostEvictionCallbacks);
167+
return result;
168+
});
169+
141170
try
142171
{
143-
cacheItem = CacheProvider.GetOrCreate<object>(key, entry =>
144-
new AsyncLazy<T>(() =>
145-
{
146-
var result = addItemFactory(entry);
147-
EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy<T>(entry.PostEvictionCallbacks);
148-
return result;
149-
})
150-
);
172+
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
151173
}
152174
finally
153175
{
@@ -156,7 +178,26 @@ await locker.WaitAsync()
156178

157179
try
158180
{
159-
var result = GetValueFromAsyncLazy<T>(cacheItem);
181+
var result = GetValueFromAsyncLazy<T>(cacheItem, out var valueHasChangedType);
182+
183+
// if we get a cache hit but for something with the wrong type we need to evict it, start again and cache the new item instead
184+
if (valueHasChangedType)
185+
{
186+
CacheProvider.Remove(key);
187+
await locker.WaitAsync()
188+
.ConfigureAwait(
189+
false); //TODO: do we really need to lock everything here - faster if we could lock on just the key?
190+
try
191+
{
192+
cacheItem = CacheProvider.GetOrCreate<object>(key, CacheFactory);
193+
}
194+
finally
195+
{
196+
locker.Release();
197+
}
198+
result = GetValueFromAsyncLazy<T>(cacheItem, out _ /* we just evicted so type change cannot happen this time */);
199+
}
200+
160201

161202
if (result.IsCanceled || result.IsFaulted)
162203
CacheProvider.Remove(key);
@@ -170,8 +211,9 @@ await locker.WaitAsync()
170211
}
171212
}
172213

173-
protected virtual T GetValueFromLazy<T>(object item)
214+
protected virtual T GetValueFromLazy<T>(object item, out bool valueHasChangedType)
174215
{
216+
valueHasChangedType = false;
175217
switch (item)
176218
{
177219
case Lazy<T> lazy:
@@ -187,11 +229,21 @@ protected virtual T GetValueFromLazy<T>(object item)
187229
return task.Result;
188230
}
189231

232+
// if they have cached something else with the same key we need to tell caller to reset the cached item
233+
// although this is probably not the fastest this should not get called on the main use case
234+
// where you just hit the first switch case above.
235+
var itemsType = item?.GetType();
236+
if (itemsType != null && itemsType.IsGenericType && itemsType.GetGenericTypeDefinition() == typeof(Lazy<>))
237+
{
238+
valueHasChangedType = true;
239+
}
240+
190241
return default(T);
191242
}
192243

193-
protected virtual Task<T> GetValueFromAsyncLazy<T>(object item)
244+
protected virtual Task<T> GetValueFromAsyncLazy<T>(object item, out bool valueHasChangedType)
194245
{
246+
valueHasChangedType = false;
195247
switch (item)
196248
{
197249
case AsyncLazy<T> asyncLazy:
@@ -205,6 +257,15 @@ protected virtual Task<T> GetValueFromAsyncLazy<T>(object item)
205257
return Task.FromResult(variable);
206258
}
207259

260+
// if they have cached something else with the same key we need to tell caller to reset the cached item
261+
// although this is probably not the fastest this should not get called on the main use case
262+
// where you just hit the first switch case above.
263+
var itemsType = item?.GetType();
264+
if (itemsType != null && itemsType.IsGenericType && itemsType.GetGenericTypeDefinition() == typeof(AsyncLazy<>))
265+
{
266+
valueHasChangedType = true;
267+
}
268+
208269
return Task.FromResult(default(T));
209270
}
210271

0 commit comments

Comments
 (0)