From 42ab9514fc24584f896e5da899f1f2a7ec87fec4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:13:49 +0000 Subject: [PATCH 1/2] Initial plan From 0976ea96f2733095a95a0c79369a845d0b87d8e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:47:10 +0000 Subject: [PATCH 2/2] Add ShouldNotStore flag to CacheEntry Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- ...crosoft.Extensions.Caching.Abstractions.cs | 1 + .../src/CompatibilitySuppressions.xml | 70 ++++++++++ .../src/ICacheEntry.cs | 7 + .../src/CacheEntry.cs | 11 +- .../tests/MemoryCacheSetAndRemoveTests.cs | 128 ++++++++++++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Caching.Abstractions/src/CompatibilitySuppressions.xml diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs index 2545c9d147aba9..484544ab1ce9b3 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs @@ -104,6 +104,7 @@ public partial interface ICacheEntry : System.IDisposable System.Collections.Generic.IList PostEvictionCallbacks { get; } Microsoft.Extensions.Caching.Memory.CacheItemPriority Priority { get; set; } long? Size { get; set; } + bool ShouldNotStore { get; set; } System.TimeSpan? SlidingExpiration { get; set; } object? Value { get; set; } } diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/CompatibilitySuppressions.xml b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/CompatibilitySuppressions.xml new file mode 100644 index 00000000000000..7e2d3f0be0102e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/CompatibilitySuppressions.xml @@ -0,0 +1,70 @@ + + + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.get_ShouldNotStore + ref/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.set_ShouldNotStore(System.Boolean) + ref/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.get_ShouldNotStore + ref/net462/Microsoft.Extensions.Caching.Abstractions.dll + lib/net462/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.set_ShouldNotStore(System.Boolean) + ref/net462/Microsoft.Extensions.Caching.Abstractions.dll + lib/net462/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.get_ShouldNotStore + ref/net8.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net8.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.set_ShouldNotStore(System.Boolean) + ref/net8.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net8.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.get_ShouldNotStore + ref/net9.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.set_ShouldNotStore(System.Boolean) + ref/net9.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.get_ShouldNotStore + ref/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0002 + M:Microsoft.Extensions.Caching.Memory.ICacheEntry.set_ShouldNotStore(System.Boolean) + ref/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.dll + + + CP0006 + P:Microsoft.Extensions.Caching.Memory.ICacheEntry.ShouldNotStore + ref/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/ICacheEntry.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/ICacheEntry.cs index 7ea2c8c8595b3b..e0bc5b299a10ca 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/ICacheEntry.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/ICacheEntry.cs @@ -59,5 +59,12 @@ public interface ICacheEntry : IDisposable /// Gets or set the size of the cache entry value. /// long? Size { get; set; } + + /// + /// Gets or sets a value indicating whether the cache entry should not be stored in the cache. + /// When set to true, the entry will not be added to the cache and subsequent + /// requests will re-execute the factory method. + /// + bool ShouldNotStore { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs index 96b7daed712118..1f45d74891f6e0 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs @@ -30,6 +30,7 @@ internal sealed partial class CacheEntry : ICacheEntry private bool _isDisposed; private bool _isExpired; private bool _isValueSet; + private bool _shouldNotStore; private byte _evictionReason; private byte _priority = (byte)CacheItemPriority.Normal; @@ -170,6 +171,12 @@ public TimeSpan? SlidingExpiration } } + public bool ShouldNotStore + { + get => _shouldNotStore; + set => _shouldNotStore = value; + } + public object Key { get; } public object? Value @@ -196,7 +203,7 @@ public void Dispose() { CommitWithTracking(); } - else if (_isValueSet) + else if (_isValueSet && !_shouldNotStore) { _cache.SetEntry(this); } @@ -211,7 +218,7 @@ private void CommitWithTracking() // 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, // so don't use this entry. - if (_isValueSet) + if (_isValueSet && !_shouldNotStore) { _cache.SetEntry(this); diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs index 57c3f56913b60d..f4d7c0da81eb5f 100644 --- a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs +++ b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheSetAndRemoveTests.cs @@ -865,6 +865,134 @@ public void MixedKeysUsage() Assert.Equal("decimal value", cache.Get(key1)); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetOrCreate_WithShouldNotStore_DoesNotCache(bool trackLinkedCacheEntries) + { + var cache = CreateCache(trackLinkedCacheEntries); + string key = "myKey"; + int callCount = 0; + + var result1 = cache.GetOrCreate(key, entry => + { + callCount++; + entry.ShouldNotStore = true; + return "value1"; + }); + + Assert.Equal("value1", result1); + Assert.Equal(1, callCount); + Assert.False(cache.TryGetValue(key, out _)); + + var result2 = cache.GetOrCreate(key, entry => + { + callCount++; + return "value2"; + }); + + Assert.Equal("value2", result2); + Assert.Equal(2, callCount); + Assert.True(cache.TryGetValue(key, out string cachedValue)); + Assert.Equal("value2", cachedValue); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetOrCreateAsync_WithShouldNotStore_DoesNotCache(bool trackLinkedCacheEntries) + { + var cache = CreateCache(trackLinkedCacheEntries); + string key = "myKey"; + int callCount = 0; + + var result1 = await cache.GetOrCreateAsync(key, async entry => + { + callCount++; + entry.ShouldNotStore = true; + await Task.Yield(); + return "value1"; + }); + + Assert.Equal("value1", result1); + Assert.Equal(1, callCount); + Assert.False(cache.TryGetValue(key, out _)); + + var result2 = await cache.GetOrCreateAsync(key, async entry => + { + callCount++; + await Task.Yield(); + return "value2"; + }); + + Assert.Equal("value2", result2); + Assert.Equal(2, callCount); + Assert.True(cache.TryGetValue(key, out string cachedValue)); + Assert.Equal("value2", cachedValue); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetOrCreate_WithoutShouldNotStore_DoesCache(bool trackLinkedCacheEntries) + { + var cache = CreateCache(trackLinkedCacheEntries); + string key = "myKey"; + int callCount = 0; + + var result1 = cache.GetOrCreate(key, entry => + { + callCount++; + return "value1"; + }); + + Assert.Equal("value1", result1); + Assert.Equal(1, callCount); + Assert.True(cache.TryGetValue(key, out string cachedValue)); + Assert.Equal("value1", cachedValue); + + var result2 = cache.GetOrCreate(key, entry => + { + callCount++; + return "value2"; + }); + + Assert.Equal("value1", result2); + Assert.Equal(1, callCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetOrCreateAsync_WithoutShouldNotStore_DoesCache(bool trackLinkedCacheEntries) + { + var cache = CreateCache(trackLinkedCacheEntries); + string key = "myKey"; + int callCount = 0; + + var result1 = await cache.GetOrCreateAsync(key, async entry => + { + callCount++; + await Task.Yield(); + return "value1"; + }); + + Assert.Equal("value1", result1); + Assert.Equal(1, callCount); + Assert.True(cache.TryGetValue(key, out string cachedValue)); + Assert.Equal("value1", cachedValue); + + var result2 = await cache.GetOrCreateAsync(key, async entry => + { + callCount++; + await Task.Yield(); + return "value2"; + }); + + Assert.Equal("value1", result2); + Assert.Equal(1, callCount); + } + private class TestKey { public override bool Equals(object obj) => true;