diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs
new file mode 100644
index 0000000000000..fcc1284ccccdf
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs
@@ -0,0 +1,6 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly
+// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp Microsoft.Extensions.Caching.Abstractions
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
\ No newline at end of file
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 2805ac7644c9d..3bda5a0a27b00 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
@@ -126,6 +126,14 @@ public MemoryCacheEntryOptions() { }
public long? Size { get { throw null; } set { } }
public System.TimeSpan? SlidingExpiration { get { throw null; } set { } }
}
+ public partial class MemoryCacheStatistics
+ {
+ public MemoryCacheStatistics() { }
+ public long CurrentEntryCount { get { throw null; } init { } }
+ public long? CurrentEstimatedSize { get { throw null; } init { } }
+ public long TotalHits { get { throw null; } init { } }
+ public long TotalMisses { get { throw null; } init { } }
+ }
public partial class PostEvictionCallbackRegistration
{
public PostEvictionCallbackRegistration() { }
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.csproj
index 9d967d4f3adb2..36666f11889e4 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.csproj
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.csproj
@@ -6,6 +6,10 @@
+
+
+
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.net6.0.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.net6.0.cs
new file mode 100644
index 0000000000000..d3812319a7e19
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.net6.0.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// ------------------------------------------------------------------------------
+// Changes to this file must follow the https://aka.ms/api-review process.
+// ------------------------------------------------------------------------------
+
+namespace Microsoft.Extensions.Caching.Memory
+{
+ public partial interface IMemoryCache : System.IDisposable
+ {
+ MemoryCacheStatistics? GetCurrentStatistics() => null;
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs
index 88576eba04c74..ccce249b23d95 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs
@@ -30,5 +30,13 @@ public interface IMemoryCache : IDisposable
///
/// An object identifying the entry.
void Remove(object key);
+
+#if NET6_0_OR_GREATER
+ ///
+ /// Gets a snapshot of the cache statistics if available.
+ ///
+ /// An instance of containing a snapshot of the cache statistics.
+ MemoryCacheStatistics? GetCurrentStatistics() => null;
+#endif
}
}
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheStatistics.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheStatistics.cs
new file mode 100644
index 0000000000000..2040522c0b46b
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheStatistics.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.Extensions.Caching.Memory
+{
+ ///
+ /// Holds a snapshot of statistics for a memory cache.
+ ///
+ public class MemoryCacheStatistics
+ {
+ ///
+ /// Initializes an instance of MemoryCacheStatistics.
+ ///
+ public MemoryCacheStatistics() { }
+
+ ///
+ /// Gets the number of instances currently in the memory cache.
+ ///
+ public long CurrentEntryCount { get; init; }
+
+ ///
+ /// Gets an estimated sum of all the values currently in the memory cache.
+ ///
+ /// Returns if size isn't being tracked. The common MemoryCache implementation tracks size whenever a SizeLimit is set on the cache.
+ public long? CurrentEstimatedSize { get; init; }
+
+ ///
+ /// Gets the total number of cache misses.
+ ///
+ public long TotalMisses { get; init; }
+
+ ///
+ /// Gets the total number of cache hits.
+ ///
+ public long TotalHits { get; init; }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs
new file mode 100644
index 0000000000000..b6510c56d117a
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs
@@ -0,0 +1,6 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly
+// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp Microsoft.Extensions.Caching.Abstractions
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
diff --git a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.csproj
index ddd5093c1436a..25d77c9de8c99 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.csproj
+++ b/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Microsoft.Extensions.Caching.Abstractions.csproj
@@ -18,4 +18,9 @@ Microsoft.Extensions.Caching.Memory.IMemoryCache
+
+
+
+
+
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 e61a5b0be20ba..7a13cdf999acb 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
@@ -33,6 +33,7 @@ public void Compact(double percentage) { }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
~MemoryCache() { }
+ public Microsoft.Extensions.Caching.Memory.MemoryCacheStatistics? GetCurrentStatistics() { throw null; }
public void Remove(object key) { }
public bool TryGetValue(object key, out object? result) { throw null; }
}
@@ -45,6 +46,7 @@ public MemoryCacheOptions() { }
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 bool TrackStatistics { get { throw null; } set { } }
}
public partial class MemoryDistributedCacheOptions : Microsoft.Extensions.Caching.Memory.MemoryCacheOptions
{
diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
index 182cea820b063..127b2250f592c 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
+++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
@@ -25,6 +25,9 @@ public class MemoryCache : IMemoryCache
private readonly MemoryCacheOptions _options;
+ private readonly List>? _allStats;
+ private readonly Stats? _accumulatedStats;
+ private readonly ThreadLocal? _stats;
private CoherentState _coherentState;
private bool _disposed;
private DateTimeOffset _lastExpirationScan;
@@ -53,6 +56,13 @@ public MemoryCache(IOptions optionsAccessor!!, ILoggerFactor
_options.Clock = new SystemClock();
}
+ if (_options.TrackStatistics)
+ {
+ _allStats = new List>();
+ _accumulatedStats = new Stats();
+ _stats = new ThreadLocal(() => new Stats(this));
+ }
+
_lastExpirationScan = _options.Clock.UtcNow;
TrackLinkedCacheEntries = _options.TrackLinkedCacheEntries; // we store the setting now so it's consistent for entire MemoryCache lifetime
}
@@ -219,6 +229,14 @@ public bool TryGetValue(object key!!, out object? result)
}
StartScanForExpiredItemsIfNeeded(utcNow);
+ // Hit
+ if (_allStats is not null)
+ {
+ if (IntPtr.Size == 4)
+ Interlocked.Increment(ref GetStats().Hits);
+ else
+ GetStats().Hits++;
+ }
return true;
}
@@ -232,6 +250,15 @@ public bool TryGetValue(object key!!, out object? result)
StartScanForExpiredItemsIfNeeded(utcNow);
result = null;
+ // Miss
+ if (_allStats is not null)
+ {
+ if (IntPtr.Size == 4)
+ Interlocked.Increment(ref GetStats().Misses);
+ else
+ GetStats().Misses++;
+ }
+
return false;
}
@@ -270,6 +297,27 @@ public void Clear()
}
}
+ ///
+ /// Gets a snapshot of the current statistics for the memory cache.
+ ///
+ /// Returns if statistics are not being tracked because is .
+ public MemoryCacheStatistics? GetCurrentStatistics()
+ {
+ if (_allStats is not null)
+ {
+ (long hit, long miss) sumTotal = Sum();
+ return new MemoryCacheStatistics()
+ {
+ TotalMisses = sumTotal.miss,
+ TotalHits = sumTotal.hit,
+ CurrentEntryCount = Count,
+ CurrentEstimatedSize = _options.SizeLimit.HasValue ? Size : null
+ };
+ }
+
+ return null;
+ }
+
internal void EntryExpired(CacheEntry entry)
{
// TODO: For efficiency consider processing these expirations in batches.
@@ -295,6 +343,72 @@ void ScheduleTask(DateTimeOffset utcNow)
}
}
+ private (long, long) Sum()
+ {
+ lock (_allStats!)
+ {
+ long hits = _accumulatedStats!.Hits;
+ long misses = _accumulatedStats.Misses;
+
+ foreach (WeakReference wr in _allStats)
+ {
+ if (wr.TryGetTarget(out Stats? stats))
+ {
+ hits += Interlocked.Read(ref stats.Hits);
+ misses += Interlocked.Read(ref stats.Misses);
+ }
+ }
+
+ return (hits, misses);
+ }
+ }
+
+ private Stats GetStats() => _stats!.Value!;
+
+ internal sealed class Stats
+ {
+ private readonly MemoryCache? _memoryCache;
+ public long Hits;
+ public long Misses;
+
+ public Stats() { }
+
+ public Stats(MemoryCache memoryCache)
+ {
+ _memoryCache = memoryCache;
+ _memoryCache.AddToStats(this);
+ }
+
+ ~Stats() => _memoryCache?.RemoveFromStats(this);
+ }
+
+ private void RemoveFromStats(Stats current)
+ {
+ lock (_allStats!)
+ {
+ for (int i = 0; i < _allStats.Count; i++)
+ {
+ if (_allStats[i].TryGetTarget(out Stats? stats) && stats == current)
+ {
+ _allStats.RemoveAt(i);
+ break;
+ }
+ }
+
+ _accumulatedStats!.Hits += Interlocked.Read(ref current.Hits);
+ _accumulatedStats.Misses += Interlocked.Read(ref current.Misses);
+ _allStats.TrimExcess();
+ }
+ }
+
+ private void AddToStats(Stats current)
+ {
+ lock (_allStats!)
+ {
+ _allStats.Add(new WeakReference(current));
+ }
+ }
+
private static void ScanForExpiredItems(MemoryCache cache)
{
DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock!.UtcNow;
@@ -469,6 +583,7 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
+ _stats?.Dispose();
GC.SuppressFinalize(this);
}
diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs
index 82968cefd33b1..a37111ad60749 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs
+++ b/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCacheOptions.cs
@@ -59,6 +59,11 @@ public double CompactionPercentage
/// Prior to .NET 7 this feature was always enabled.
public bool TrackLinkedCacheEntries { get; set; }
+ ///
+ /// Gets or sets whether to track memory cache statistics. Disabled by default.
+ ///
+ public bool TrackStatistics { get; set; }
+
MemoryCacheOptions IOptions.Value
{
get { return this; }
diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheGetCurrentStatisticsTests.cs b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheGetCurrentStatisticsTests.cs
new file mode 100644
index 0000000000000..cee6245f3e9c6
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/MemoryCacheGetCurrentStatisticsTests.cs
@@ -0,0 +1,137 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Caching.Memory.Infrastructure;
+using Xunit;
+
+namespace Microsoft.Extensions.Caching.Memory
+{
+ public class MemoryCacheHasStatisticsTests
+ {
+ [Fact]
+ public void GetCurrentStatistics_TrackStatisticsFalse_ReturnsNull()
+ {
+ var cache = new MemoryCache(new MemoryCacheOptions { TrackStatistics = false });
+ Assert.Null(cache.GetCurrentStatistics());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetCurrentStatistics_GetCache_UpdatesStatistics(bool sizeLimitIsSet)
+ {
+ var cache = sizeLimitIsSet ?
+ new MemoryCache(new MemoryCacheOptions { TrackStatistics = true, SizeLimit = 10 }) :
+ new MemoryCache(new MemoryCacheOptions { TrackStatistics = true });
+
+ cache.Set("key", "value", new MemoryCacheEntryOptions { Size = 2 });
+ for (int i = 0; i < 100; i++)
+ {
+ Assert.Equal("value", cache.Get("key"));
+ Assert.Null(cache.Get("missingKey1"));
+ Assert.Null(cache.Get("missingKey2"));
+ }
+
+ MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
+
+ Assert.NotNull(stats);
+ Assert.Equal(200, stats.TotalMisses);
+ Assert.Equal(100, stats.TotalHits);
+ Assert.Equal(1, stats.CurrentEntryCount);
+ VerifyCurrentEstimatedSize(2, sizeLimitIsSet, stats);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetCurrentStatistics_UpdateExistingCache_UpdatesStatistics(bool sizeLimitIsSet)
+ {
+ var cache = sizeLimitIsSet ?
+ new MemoryCache(new MemoryCacheOptions { TrackStatistics = true, SizeLimit = 10 }) :
+ new MemoryCache(new MemoryCacheOptions { TrackStatistics = true });
+
+ cache.Set("key", "value", new MemoryCacheEntryOptions { Size = 2 });
+ Assert.Equal("value", cache.Get("key"));
+
+ cache.Set("key", "updated value", new MemoryCacheEntryOptions { Size = 3 });
+ Assert.Equal("updated value", cache.Get("key"));
+
+ MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
+
+ Assert.NotNull(stats);
+ Assert.Equal(1, stats.CurrentEntryCount);
+ Assert.Equal(0, stats.TotalMisses);
+ Assert.Equal(2, stats.TotalHits);
+ VerifyCurrentEstimatedSize(3, sizeLimitIsSet, stats);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GetCurrentStatistics_UpdateAfterExistingItemExpired_CurrentEstimatedSizeResets(bool sizeLimitIsSet)
+ {
+ const string Key = "myKey";
+
+ var cache = new MemoryCache(sizeLimitIsSet ?
+ new MemoryCacheOptions { TrackStatistics = true, Clock = new SystemClock(), SizeLimit = 10 } :
+ new MemoryCacheOptions { TrackStatistics = true, Clock = new SystemClock() }
+ );
+
+ ICacheEntry entry;
+ using (entry = cache.CreateEntry(Key))
+ {
+ var expirationToken = new TestExpirationToken() { ActiveChangeCallbacks = true };
+ var mc = new MemoryCacheEntryOptions { Size = 5 };
+ cache.Set(Key, new object(), mc.AddExpirationToken(expirationToken));
+ MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
+
+ Assert.NotNull(stats);
+ Assert.Equal(1, cache.Count);
+ Assert.Equal(1, stats.CurrentEntryCount);
+ VerifyCurrentEstimatedSize(5, sizeLimitIsSet, stats);
+
+ expirationToken.HasChanged = true;
+ cache.Set(Key, new object(), mc.AddExpirationToken(expirationToken));
+ stats = cache.GetCurrentStatistics();
+
+ Assert.NotNull(stats);
+ Assert.Equal(0, cache.Count);
+ Assert.Equal(0, stats.CurrentEntryCount);
+ VerifyCurrentEstimatedSize(0, sizeLimitIsSet, stats);
+ }
+ }
+
+#if NET6_0_OR_GREATER
+ [Fact]
+ public void GetCurrentStatistics_DIMReturnsNull()
+ {
+ Assert.Null((new FakeMemoryCache() as IMemoryCache).GetCurrentStatistics());
+ }
+#endif
+
+ private class FakeMemoryCache : IMemoryCache
+ {
+ public ICacheEntry CreateEntry(object key) => throw new NotImplementedException();
+ public void Dispose() => throw new NotImplementedException();
+ public void Remove(object key) => throw new NotImplementedException();
+ public bool TryGetValue(object key, out object? value) => throw new NotImplementedException();
+ }
+
+ private void VerifyCurrentEstimatedSize(long expected, bool sizeLimitIsSet, MemoryCacheStatistics stats)
+ {
+ if (sizeLimitIsSet)
+ {
+ Assert.Equal(expected, stats.CurrentEstimatedSize );
+ }
+ else
+ {
+ Assert.Null(stats.CurrentEstimatedSize );
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/Microsoft.Extensions.Caching.Memory.Tests.csproj b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/Microsoft.Extensions.Caching.Memory.Tests.csproj
index 9773db13ad771..429cde4c0db43 100644
--- a/src/libraries/Microsoft.Extensions.Caching.Memory/tests/Microsoft.Extensions.Caching.Memory.Tests.csproj
+++ b/src/libraries/Microsoft.Extensions.Caching.Memory/tests/Microsoft.Extensions.Caching.Memory.Tests.csproj
@@ -11,7 +11,8 @@
-
+
+