Skip to content

Commit c9f45ce

Browse files
authored
Complete Cache/CacheDependency implementation and add additional tests. (#351)
* Complete Cache/CacheDependency implementation and add additional tests. * Add support for and tests for Cache.Insert(String, Object, CacheDependency) * Fix issue when there are no cachedependencies when calculating utcLastModified * Refactor cache insert code to make it cleaner and use a common code path to ensure ChangeMonitors are always handled. * To support custom CacheDependency types that might not use change monitors under the surface, always call GetChangeMonitor and add it to the policy's change monitors.
1 parent 7d7caf8 commit c9f45ce

File tree

5 files changed

+184
-25
lines changed

5 files changed

+184
-25
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters/Caching/Cache.cs

+7-17
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,14 @@ public object Add(string key, object value, CacheDependency? dependencies, DateT
4545
Priority = Convert(priority),
4646
RemovedCallback = Convert(onRemoveCallback),
4747
};
48-
49-
return _cache.AddOrGetExisting(key, value, policy);
50-
}
51-
52-
private static void AddChangeMonitors(CacheDependency? dependencies, CacheItemPolicy policy)
53-
{
54-
if (dependencies?.ChangeMonitors is not null)
55-
{
56-
policy.ChangeMonitors.Add(dependencies.GetChangeMonitor());
57-
}
48+
return _cache.AddOrGetExisting(key, value, policy, dependencies);
5849
}
5950

6051
public object Get(string key) => _cache.Get(key);
6152

62-
public void Insert(string key, object value) => _cache.Set(key, value, new CacheItemPolicy());
53+
public void Insert(string key, object value) => _cache.Set(key, value);
54+
55+
public void Insert(string key, object value, CacheDependency? dependencies) => _cache.Set(key, value, dependencies);
6356

6457
public void Insert(string key, object value, CacheDependency? dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)
6558
{
@@ -68,8 +61,7 @@ public void Insert(string key, object value, CacheDependency? dependencies, Date
6861
AbsoluteExpiration = Convert(absoluteExpiration),
6962
SlidingExpiration = slidingExpiration,
7063
};
71-
72-
_cache.Set(key, value, policy);
64+
_cache.Set(key, value, policy, dependencies);
7365
}
7466

7567
public void Insert(string key, object value, CacheDependency? dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback? onRemoveCallback)
@@ -81,8 +73,7 @@ public void Insert(string key, object value, CacheDependency? dependencies, Date
8173
Priority = Convert(priority),
8274
RemovedCallback = Convert(onRemoveCallback),
8375
};
84-
85-
_cache.Set(key, value, policy);
76+
_cache.Set(key, value, policy, dependencies);
8677
}
8778

8879
public void Insert(string key, object value, CacheDependency? dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback)
@@ -93,8 +84,7 @@ public void Insert(string key, object value, CacheDependency? dependencies, Date
9384
SlidingExpiration = slidingExpiration,
9485
UpdateCallback = Convert(onUpdateCallback),
9586
};
96-
97-
_cache.Set(key, value, policy);
87+
_cache.Set(key, value, policy, dependencies);
9888
}
9989

10090
public object? Remove(string key) => _cache.Remove(key);

src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs

+9-8
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ public class CacheDependency : IDisposable
1818
private string? uniqueId;
1919
private bool uniqueIdInitialized;
2020

21-
internal CacheDependency()
22-
{
23-
FinishInit();
24-
}
21+
protected CacheDependency() => FinishInit();
2522

2623
public CacheDependency(string filename) : this(filename, DateTime.MaxValue) { }
2724

@@ -31,9 +28,13 @@ public CacheDependency(string[] filenames) : this(filenames, null, null, DateTim
3128

3229
public CacheDependency(string[] filenames, DateTime start) : this(filenames, null, null, start) { }
3330

31+
public CacheDependency(string[]? filenames, string[]? cachekeys) : this(filenames, cachekeys, null, DateTime.MaxValue) { }
32+
3433
public CacheDependency(string[]? filenames, string[]? cachekeys, DateTime start) :
35-
this(filenames, cachekeys, null, start)
36-
{ }
34+
this(filenames, cachekeys, null, start) { }
35+
36+
public CacheDependency(string[]? filenames, string[]? cachekeys, CacheDependency? dependency) :
37+
this(filenames, cachekeys, dependency, DateTime.MaxValue) { }
3738

3839
public CacheDependency(
3940
string[]? filenames,
@@ -69,7 +70,7 @@ public CacheDependency(
6970
protected internal void FinishInit()
7071
{
7172
HasChanged = changeMonitors.Any(cm => cm.HasChanged && (cm.GetLastModifiedUtc() > utcStart));
72-
utcLastModified = changeMonitors.Max(cm => cm.GetLastModifiedUtc());
73+
utcLastModified = changeMonitors.Count==0 ? DateTime.MinValue : changeMonitors.Max(cm => cm.GetLastModifiedUtc());
7374
if (HasChanged)
7475
{
7576
NotifyDependencyChanged(this, EventArgs.Empty);
@@ -89,7 +90,7 @@ private class ChangeNotificationEventArgs : EventArgs
8990

9091
protected void NotifyDependencyChanged(object sender, EventArgs e)
9192
{
92-
if (initCompleted && DateTime.UtcNow > utcStart)
93+
if (initCompleted && (utcStart == DateTime.MaxValue || DateTime.UtcNow > utcStart))
9394
{
9495
HasChanged = true;
9596
utcLastModified = DateTime.UtcNow;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Runtime.Caching;
2+
3+
namespace System.Web.Caching;
4+
5+
internal static class CacheExtensions
6+
{
7+
public static CacheItemPolicy WithCacheDependency(this CacheItemPolicy policy, CacheDependency? dependency)
8+
{
9+
if (dependency is not null)
10+
{
11+
policy.ChangeMonitors.Add(dependency.GetChangeMonitor());
12+
}
13+
return policy;
14+
}
15+
16+
public static void Set(this ObjectCache cache, string key, object value) =>
17+
cache.Set(key, value, new CacheItemPolicy());
18+
19+
public static void Set(this ObjectCache cache, string key, object value,
20+
CacheDependency? dependency) =>
21+
cache.Set(key, value, new CacheItemPolicy().WithCacheDependency(dependency));
22+
23+
public static void Set(this ObjectCache cache, string key, object value,
24+
CacheItemPolicy policy, CacheDependency? dependency) =>
25+
cache.Set(key, value, policy.WithCacheDependency(dependency));
26+
27+
public static object AddOrGetExisting(this ObjectCache cache, string key, object value,
28+
CacheItemPolicy policy, CacheDependency? dependency) =>
29+
cache.AddOrGetExisting(key, value, policy.WithCacheDependency(dependency));
30+
31+
}

src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs

+4
Original file line numberDiff line numberDiff line change
@@ -662,18 +662,22 @@ public sealed partial class Cache : System.Collections.IEnumerable
662662
public object Get(string key) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
663663
public System.Collections.IEnumerator GetEnumerator() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
664664
public void Insert(string key, object value) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
665+
public void Insert(string key, object value, System.Web.Caching.CacheDependency dependencies) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
665666
public void Insert(string key, object value, System.Web.Caching.CacheDependency dependencies, System.DateTime absoluteExpiration, System.TimeSpan slidingExpiration) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
666667
public void Insert(string key, object value, System.Web.Caching.CacheDependency dependencies, System.DateTime absoluteExpiration, System.TimeSpan slidingExpiration, System.Web.Caching.CacheItemPriority priority, System.Web.Caching.CacheItemRemovedCallback onRemoveCallback) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
667668
public void Insert(string key, object value, System.Web.Caching.CacheDependency dependencies, System.DateTime absoluteExpiration, System.TimeSpan slidingExpiration, System.Web.Caching.CacheItemUpdateCallback onUpdateCallback) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
668669
public object Remove(string key) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
669670
}
670671
public partial class CacheDependency : System.IDisposable
671672
{
673+
protected CacheDependency() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
672674
public CacheDependency(string filename) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
673675
public CacheDependency(string filename, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
674676
public CacheDependency(string[] filenames) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
675677
public CacheDependency(string[] filenames, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
678+
public CacheDependency(string[] filenames, string[] cachekeys) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
676679
public CacheDependency(string[] filenames, string[] cachekeys, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
680+
public CacheDependency(string[] filenames, string[] cachekeys, System.Web.Caching.CacheDependency dependency) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
677681
public CacheDependency(string[] filenames, string[] cachekeys, System.Web.Caching.CacheDependency dependency, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
678682
public bool HasChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
679683
public System.DateTime UtcLastModified { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }

test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Caching/CacheTests.cs

+133
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections;
5+
using System.Collections.Generic;
56
using System.Runtime.Caching;
67
using System.Threading.Tasks;
78
using AutoFixture;
9+
using Microsoft.AspNetCore.SystemWebAdapters;
810
using Moq;
911
using Xunit;
1012

@@ -292,4 +294,135 @@ public void InsertItem()
292294
}
293295

294296
private sealed record Removal(string Key, object Item, CacheItemRemovedReason Reason);
297+
298+
[Fact]
299+
public void InsertNoAbsoluteSlidingExpiration()
300+
{
301+
// Arrange
302+
var memCache = new Mock<MemoryCache>(_fixture.Create<string>(), null);
303+
var cache = new Cache(memCache.Object);
304+
var key = _fixture.Create<string>();
305+
var item = new object();
306+
307+
// Act
308+
cache.Insert(key, item);
309+
310+
// Assert
311+
memCache.Verify(m => m.Set(key, item, It.Is<CacheItemPolicy>(e => e.AbsoluteExpiration.Equals(Cache.NoAbsoluteExpiration) && e.SlidingExpiration.Equals(Cache.NoSlidingExpiration)), null), Times.Once);
312+
}
313+
314+
[Fact]
315+
public void InsertWithDependency()
316+
{
317+
// Arrange
318+
var memCache = new Mock<MemoryCache>(_fixture.Create<string>(), null);
319+
var cache = new Cache(memCache.Object);
320+
var key = _fixture.Create<string>();
321+
var item = new object();
322+
var cacheDependency = new Mock<CacheDependency>();
323+
324+
// Act
325+
cache.Insert(key, item, cacheDependency.Object);
326+
327+
// Assert
328+
memCache.Verify(m => m.Set(key, item, It.Is<CacheItemPolicy>(e => e.AbsoluteExpiration.Equals(Cache.NoAbsoluteExpiration) && e.SlidingExpiration.Equals(Cache.NoSlidingExpiration)), null), Times.Once);
329+
}
330+
331+
[Fact]
332+
public async Task DependentFileCallback()
333+
{
334+
// Arrange
335+
using var memCache = new MemoryCache(_fixture.Create<string>());
336+
var cache = new Cache(memCache);
337+
var item = new object();
338+
var key = _fixture.Create<string>();
339+
var updated = false;
340+
var slidingExpiration = TimeSpan.FromMilliseconds(1);
341+
CacheItemUpdateReason? updateReason = default;
342+
343+
void Callback(string key, CacheItemUpdateReason reason, out object? expensiveObject, out CacheDependency? dependency, out DateTime absoluteExpiration, out TimeSpan slidingExpiration)
344+
{
345+
expensiveObject = null;
346+
dependency = null;
347+
absoluteExpiration = Cache.NoAbsoluteExpiration;
348+
slidingExpiration = TimeSpan.FromMilliseconds(5);
349+
350+
updated = true;
351+
updateReason = reason;
352+
}
353+
354+
var file = System.IO.Path.GetTempFileName();
355+
await System.IO.File.WriteAllTextAsync(file, key);
356+
357+
using var cd = new CacheDependency(file);
358+
// Act
359+
cache.Insert(key, item, cd, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, Callback);
360+
361+
// Ensure file is updated
362+
await System.IO.File.WriteAllTextAsync(file, DateTime.UtcNow.ToString("O"));
363+
364+
// Small delay here to ensure that the file change notification happens (may fail tests if too fast)
365+
await Task.Delay(10);
366+
367+
// Force cleanup to initiate callbacks on current thread
368+
memCache.Trim(100);
369+
370+
// Assert
371+
Assert.True(updated);
372+
Assert.Null(cache[key]);
373+
Assert.Equal(CacheItemUpdateReason.DependencyChanged, updateReason);
374+
}
375+
376+
[Fact]
377+
public async Task DependentItemCallback()
378+
{
379+
// Arrange
380+
using var memCache = new MemoryCache(_fixture.Create<string>());
381+
382+
var cache = new Cache(memCache);
383+
var httpRuntime = new Mock<IHttpRuntime>();
384+
httpRuntime.Setup(s => s.Cache).Returns(cache);
385+
HttpRuntime.Current = httpRuntime.Object;
386+
387+
var item1 = new object();
388+
var item2 = new object();
389+
var key1 = _fixture.Create<string>();
390+
var key2 = _fixture.Create<string>();
391+
var updateReason = new Dictionary<string, CacheItemUpdateReason>();
392+
var slidingExpiration = TimeSpan.FromMilliseconds(1);
393+
394+
void Callback(string key, CacheItemUpdateReason reason, out object? expensiveObject, out CacheDependency? dependency, out DateTime absoluteExpiration, out TimeSpan slidingExpiration)
395+
{
396+
expensiveObject = null;
397+
dependency = null;
398+
absoluteExpiration = Cache.NoAbsoluteExpiration;
399+
slidingExpiration = Cache.NoSlidingExpiration;
400+
401+
updateReason[key] = reason;
402+
}
403+
404+
// Act
405+
cache.Insert(key1, item1, null, Cache.NoAbsoluteExpiration, slidingExpiration, Callback);
406+
407+
using var cd = new CacheDependency(null, new[] { key1 });
408+
cache.Insert(key2, item2, cd, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, Callback);
409+
410+
Assert.Empty(updateReason);
411+
412+
// Ensure sliding expiration has hit
413+
await Task.Delay(slidingExpiration);
414+
415+
// Force cleanup to initiate callbacks on current thread
416+
memCache.Trim(100);
417+
418+
// Assert
419+
Assert.Contains(key1, updateReason.Keys);
420+
Assert.Contains(key2, updateReason.Keys);
421+
422+
Assert.Null(cache[key1]);
423+
Assert.Null(cache[key2]);
424+
425+
Assert.Equal(CacheItemUpdateReason.Expired, updateReason[key1]);
426+
Assert.Equal(CacheItemUpdateReason.DependencyChanged, updateReason[key2]);
427+
}
295428
}

0 commit comments

Comments
 (0)