diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs index dcdce63cb52b5d..ee0d00700c54be 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs @@ -43,6 +43,7 @@ public MemoryCacheOptions() { } public System.TimeSpan ExpirationScanFrequency { get { throw null; } set { } } Microsoft.Extensions.Caching.Memory.MemoryCacheOptions Microsoft.Extensions.Options.IOptions.Value { get { throw null; } } public long? SizeLimit { get { throw null; } set { } } + public bool TrackLinkedCacheEntries { get { throw null; } set { } } } public partial class MemoryDistributedCacheOptions : Microsoft.Extensions.Caching.Memory.MemoryCacheOptions { diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs index cd3aabcf840a3a..e9974c542c2259 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs @@ -20,7 +20,7 @@ internal sealed partial class CacheEntry : ICacheEntry private TimeSpan? _absoluteExpirationRelativeToNow; private TimeSpan? _slidingExpiration; private long? _size; - private CacheEntry _previous; // this field is not null only before the entry is added to the cache + private CacheEntry _previous; // this field is not null only before the entry is added to the cache and tracking is enabled private object _value; private CacheEntryState _state; @@ -28,7 +28,7 @@ internal CacheEntry(object key, MemoryCache memoryCache) { Key = key ?? throw new ArgumentNullException(nameof(key)); _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _previous = CacheEntryHelper.EnterScope(this); + _previous = memoryCache.TrackLinkedCacheEntries ? CacheEntryHelper.EnterScope(this) : null; _state = new CacheEntryState(CacheItemPriority.Normal); } @@ -136,7 +136,10 @@ public void Dispose() { _state.IsDisposed = true; - CacheEntryHelper.ExitScope(this, _previous); + if (_cache.TrackLinkedCacheEntries) + { + CacheEntryHelper.ExitScope(this, _previous); + } // Don't commit or propagate options if the CacheEntry Value was never set. // We assume an exception occurred causing the caller to not set the Value successfully, diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs index 35121fc86ebc34..c14e9f68f11086 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs @@ -64,6 +64,7 @@ public MemoryCache(IOptions optionsAccessor, ILoggerFactory } _lastExpirationScan = _options.Clock.UtcNow; + TrackLinkedCacheEntries = _options.TrackLinkedCacheEntries; // we store the setting now so it's consistent for entire MemoryCache lifetime } /// @@ -79,6 +80,8 @@ public MemoryCache(IOptions optionsAccessor, ILoggerFactory // internal for testing internal long Size { get => Interlocked.Read(ref _cacheSize); } + internal bool TrackLinkedCacheEntries { get; } + private ICollection> EntriesCollection => _entries; /// @@ -232,7 +235,7 @@ public bool TryGetValue(object key, out object result) entry.LastAccessed = utcNow; result = entry.Value; - if (entry.CanPropagateOptions()) + if (TrackLinkedCacheEntries && entry.CanPropagateOptions()) { // When this entry is retrieved in the scope of creating another entry, // that entry needs a copy of these expiration tokens. diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs index bea7aa9cffe458..9fd9dfaed4d0a9 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs @@ -53,6 +53,12 @@ public double CompactionPercentage } } + /// + /// Gets or sets whether to track linked entries. Disabled by default. + /// + /// Prior to .NET 7 this feature was always enabled. + public bool TrackLinkedCacheEntries { get; set; } + MemoryCacheOptions IOptions.Value { get { return this; } diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/CacheEntryScopeExpirationTests.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/CacheEntryScopeExpirationTests.cs index 9d34d63989f8de..c8f242e9b46bad 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/CacheEntryScopeExpirationTests.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/CacheEntryScopeExpirationTests.cs @@ -12,43 +12,48 @@ namespace Microsoft.Extensions.Caching.Memory { public class CacheEntryScopeExpirationTests { - private IMemoryCache CreateCache() + private IMemoryCache CreateCache(bool trackLinkedCacheEntries = false) { - return CreateCache(new SystemClock()); + return CreateCache(new SystemClock(), trackLinkedCacheEntries); } - private IMemoryCache CreateCache(ISystemClock clock) + private IMemoryCache CreateCache(ISystemClock clock, bool trackLinkedCacheEntries = false) { return new MemoryCache(new MemoryCacheOptions() { Clock = clock, + TrackLinkedCacheEntries = trackLinkedCacheEntries }); } - [Fact] - public void SetPopulates_ExpirationTokens_IntoScopedLink() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetPopulates_ExpirationTokens_IntoScopedLink(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; ICacheEntry entry; using (entry = cache.CreateEntry(key)) { - Assert.Same(entry, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry); var expirationToken = new TestExpirationToken() { ActiveChangeCallbacks = true }; cache.Set(key, obj, new MemoryCacheEntryOptions().AddExpirationToken(expirationToken)); } - Assert.Single(((CacheEntry)entry).ExpirationTokens); + Assert.Equal(trackLinkedCacheEntries ? 1 : 0, ((CacheEntry)entry).ExpirationTokens.Count); Assert.Null(((CacheEntry)entry).AbsoluteExpiration); } - [Fact] - public void SetPopulates_AbsoluteExpiration_IntoScopeLink() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetPopulates_AbsoluteExpiration_IntoScopeLink(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; var time = new DateTimeOffset(2051, 1, 1, 1, 1, 1, TimeSpan.Zero); @@ -56,21 +61,22 @@ public void SetPopulates_AbsoluteExpiration_IntoScopeLink() ICacheEntry entry; using (entry = cache.CreateEntry(key)) { - Assert.Same(entry, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry); var expirationToken = new TestExpirationToken() { ActiveChangeCallbacks = true }; cache.Set(key, obj, new MemoryCacheEntryOptions().SetAbsoluteExpiration(time)); } Assert.Empty(((CacheEntry)entry).ExpirationTokens); - Assert.NotNull(((CacheEntry)entry).AbsoluteExpiration); - Assert.Equal(time, ((CacheEntry)entry).AbsoluteExpiration); + Assert.Equal(trackLinkedCacheEntries ? time : null, ((CacheEntry)entry).AbsoluteExpiration); } - [Fact] - public void TokenExpires_LinkedEntry() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TokenExpires_LinkedEntry(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -89,13 +95,15 @@ public void TokenExpires_LinkedEntry() expirationToken.Fire(); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); } - [Fact] - public void TokenExpires_GetInLinkedEntry() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TokenExpires_GetInLinkedEntry(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -118,13 +126,15 @@ public void TokenExpires_GetInLinkedEntry() expirationToken.Fire(); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); } - [Fact] - public void TokenExpires_ParentScopeEntry() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TokenExpires_ParentScopeEntry(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -147,13 +157,15 @@ public void TokenExpires_ParentScopeEntry() expirationToken.Fire(); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); } - [Fact] - public void TokenExpires_ParentScopeEntry_WithFactory() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TokenExpires_ParentScopeEntry_WithFactory(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -176,13 +188,15 @@ public void TokenExpires_ParentScopeEntry_WithFactory() expirationToken.Fire(); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); } - [Fact] - public void TokenDoesntExpire_SiblingScopeEntry() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TokenDoesntExpire_SiblingScopeEntry(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -212,15 +226,17 @@ public void TokenDoesntExpire_SiblingScopeEntry() expirationToken.Fire(); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); Assert.True(cache.TryGetValue(key2, out value)); } - [Fact] - public void AbsoluteExpiration_WorksAcrossLink() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AbsoluteExpiration_WorksAcrossLink(bool trackLinkedCacheEntries) { var clock = new TestClock(); - var cache = CreateCache(clock); + var cache = CreateCache(clock, trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -238,14 +254,16 @@ public void AbsoluteExpiration_WorksAcrossLink() clock.Add(TimeSpan.FromSeconds(10)); Assert.False(cache.TryGetValue(key1, out object value)); - Assert.False(cache.TryGetValue(key, out value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key, out value)); } - [Fact] - public void AbsoluteExpiration_WorksAcrossNestedLink() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AbsoluteExpiration_WorksAcrossNestedLink(bool trackLinkedCacheEntries) { var clock = new TestClock(); - var cache = CreateCache(clock); + var cache = CreateCache(clock, trackLinkedCacheEntries); var obj = new object(); string key1 = "myKey1"; string key2 = "myKey2"; @@ -267,16 +285,17 @@ public void AbsoluteExpiration_WorksAcrossNestedLink() clock.Add(TimeSpan.FromSeconds(10)); - Assert.False(cache.TryGetValue(key1, out object value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key1, out object value)); Assert.False(cache.TryGetValue(key2, out value)); } - - [Fact] - public void AbsoluteExpiration_DoesntAffectSiblingLink() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AbsoluteExpiration_DoesntAffectSiblingLink(bool trackLinkedCacheEntries) { var clock = new TestClock(); - var cache = CreateCache(clock); + var cache = CreateCache(clock, trackLinkedCacheEntries); var obj = new object(); string key1 = "myKey1"; string key2 = "myKey2"; @@ -306,15 +325,17 @@ public void AbsoluteExpiration_DoesntAffectSiblingLink() clock.Add(TimeSpan.FromSeconds(10)); - Assert.False(cache.TryGetValue(key1, out object value)); + Assert.Equal(!trackLinkedCacheEntries, cache.TryGetValue(key1, out object value)); Assert.False(cache.TryGetValue(key2, out value)); Assert.True(cache.TryGetValue(key3, out value)); } - [Fact] - public void GetWithImplicitLinkPopulatesExpirationTokens() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetWithImplicitLinkPopulatesExpirationTokens(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -324,21 +345,24 @@ public void GetWithImplicitLinkPopulatesExpirationTokens() ICacheEntry entry; using (entry = cache.CreateEntry(key)) { - Assert.Same(entry, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry); + var expirationToken = new TestExpirationToken() { ActiveChangeCallbacks = true }; cache.Set(key1, obj, new MemoryCacheEntryOptions().AddExpirationToken(expirationToken)); } Assert.Null(CacheEntryHelper.Current); - Assert.Single(((CacheEntry)entry).ExpirationTokens); + Assert.Equal(trackLinkedCacheEntries ? 1 : 0, ((CacheEntry)entry).ExpirationTokens.Count); Assert.Null(((CacheEntry)entry).AbsoluteExpiration); } - [Fact] - public void LinkContextsCanNest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LinkContextsCanNest(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); var obj = new object(); string key = "myKey"; string key1 = "myKey1"; @@ -349,33 +373,35 @@ public void LinkContextsCanNest() ICacheEntry entry1; using (entry = cache.CreateEntry(key)) { - Assert.Same(entry, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry); using (entry1 = cache.CreateEntry(key1)) { - Assert.Same(entry1, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry1); var expirationToken = new TestExpirationToken() { ActiveChangeCallbacks = true }; entry1.SetValue(obj); entry1.AddExpirationToken(expirationToken); } - Assert.Same(entry, CacheEntryHelper.Current); + VerifyCurrentEntry(trackLinkedCacheEntries, entry); } Assert.Null(CacheEntryHelper.Current); Assert.Single(((CacheEntry)entry1).ExpirationTokens); Assert.Null(((CacheEntry)entry1).AbsoluteExpiration); - Assert.Single(((CacheEntry)entry).ExpirationTokens); + Assert.Equal(trackLinkedCacheEntries ? 1 : 0, ((CacheEntry)entry).ExpirationTokens.Count); Assert.Null(((CacheEntry)entry).AbsoluteExpiration); } - [Fact] - public void NestedLinkContextsCanAggregate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NestedLinkContextsCanAggregate(bool trackLinkedCacheEntries) { var clock = new TestClock(); - var cache = CreateCache(clock); + var cache = CreateCache(clock, trackLinkedCacheEntries); var obj = new object(); string key1 = "myKey1"; string key2 = "myKey2"; @@ -402,7 +428,7 @@ public void NestedLinkContextsCanAggregate() } } - Assert.Equal(2, ((CacheEntry)entry1).ExpirationTokens.Count()); + Assert.Equal(trackLinkedCacheEntries ? 2 : 1, ((CacheEntry)entry1).ExpirationTokens.Count()); Assert.NotNull(((CacheEntry)entry1).AbsoluteExpiration); Assert.Equal(clock.UtcNow + TimeSpan.FromSeconds(10), ((CacheEntry)entry1).AbsoluteExpiration); @@ -414,7 +440,7 @@ public void NestedLinkContextsCanAggregate() [Fact] public async Task LinkContexts_AreThreadSafe() { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries: true); var key1 = new object(); var key2 = new object(); var key3 = new object(); @@ -512,5 +538,17 @@ static void SetExpiredManyTimes(CacheEntry cacheEntry) } } } + + private static void VerifyCurrentEntry(bool trackLinkedCacheEntries, ICacheEntry entry) + { + if (trackLinkedCacheEntries) + { + Assert.Same(entry, CacheEntryHelper.Current); + } + else + { + Assert.Null(CacheEntryHelper.Current); + } + } } } diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs index fb15d9732061e2..65810e352e9270 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs @@ -11,9 +11,9 @@ namespace Microsoft.Extensions.Caching.Memory { public class MemoryCacheSetAndRemoveTests { - private static IMemoryCache CreateCache() + private static IMemoryCache CreateCache(bool trackLinkedCacheEntries = false) { - return new MemoryCache(new MemoryCacheOptions()); + return new MemoryCache(new MemoryCacheOptions { TrackLinkedCacheEntries = trackLinkedCacheEntries }); } [Fact] @@ -164,10 +164,12 @@ public async Task GetOrCreateAsync_ReturnExistingValue() Assert.Same(obj, result); } - [Fact] - public void GetOrCreate_WillNotCreateEmptyValue_WhenFactoryThrows() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetOrCreate_WillNotCreateEmptyValue_WhenFactoryThrows(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); string key = "myKey"; try { @@ -186,10 +188,12 @@ public void GetOrCreate_WillNotCreateEmptyValue_WhenFactoryThrows() Assert.Null(CacheEntryHelper.Current); } - [Fact] - public async Task GetOrCreateAsync_WillNotCreateEmptyValue_WhenFactoryThrows() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetOrCreateAsync_WillNotCreateEmptyValue_WhenFactoryThrows(bool trackLinkedCacheEntries) { - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); string key = "myKey"; try { @@ -208,8 +212,10 @@ await cache.GetOrCreateAsync(key, entry => Assert.Null(CacheEntryHelper.Current); } - [Fact] - public void DisposingCacheEntryReleasesScope() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DisposingCacheEntryReleasesScope(bool trackLinkedCacheEntries) { object GetScope(ICacheEntry entry) { @@ -218,19 +224,27 @@ object GetScope(ICacheEntry entry) .GetValue(entry); } - var cache = CreateCache(); + var cache = CreateCache(trackLinkedCacheEntries); ICacheEntry first = cache.CreateEntry("myKey1"); Assert.Null(GetScope(first)); // it's the first entry, so it has no previous cache entry set ICacheEntry second = cache.CreateEntry("myKey2"); - Assert.NotNull(GetScope(second)); // it's not first, so it has previous set - Assert.Same(first, GetScope(second)); // second.previous is set to first - second.Dispose(); - Assert.Null(GetScope(second)); - first.Dispose(); - Assert.Null(GetScope(first)); + if (trackLinkedCacheEntries) + { + Assert.NotNull(GetScope(second)); // it's not first, so it has previous set + Assert.Same(first, GetScope(second)); // second.previous is set to first + + second.Dispose(); + Assert.Null(GetScope(second)); + first.Dispose(); + Assert.Null(GetScope(first)); + } + else + { + Assert.Null(GetScope(second)); // tracking not enabled, the scope is null + } } [Fact]