Skip to content

New memory cache implementation #48567

Closed as duplicate
Closed as duplicate

Description

@davidfowl

Background and Motivation

The MemoryCache implementation and interface leaves much to be desired. At the core of it, we ideally want to expose something more akin to ConcurrentDictionary<TKey, TValue> that supports expiration and can handle memory pressure. What we have right now has issues:

Proposed API

The APIs are still TBD but I'm thinking a generic memory cache.

namespace Microsoft.Extensons.Caching
{
    public class MemoryCache<TKey, TValue>
    {
        public TValue this[TKey key] { get; set; }
        public bool IsEmpty { get; }
        public int Count { get; }
        public ICollection<TKey> Keys { get; }
        public ICollection<TValue> Values { get; }
        public void Clear();
        public bool ContainsKey(TKey key);
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
        public KeyValuePair<TKey, TValue>[] ToArray();

        public bool TryAdd(TKey key, CacheEntry<TValue> value);
        public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
        public bool TryRemove(TKey key, [MaybeNullWhen(false)] out TValue value);
    }

    public class CacheEntry<TValue>
    {
        TValue Value { get; set; }
        DateTimeOffset? AbsoluteExpiration { get; set; }
        TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
        TimeSpan? SlidingExpiration { get; set; }
        IList<IChangeToken> ExpirationTokens { get; }
        IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }
        CacheItemPriority Priority { get; set; }
        long? Size { get; set; }
    }
}

I'm convinced now that this shouldn't be an interface or an abstract class but I'm open to discussion.

Usage Examples

TBD

Alternative Designs

TBD

Risks

Having 3 implementations.

cc @Tratcher @JunTaoLuo @maryamariyan @eerhardt

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Feb 21, 2021
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Feb 21, 2021
  3. adamsitnik commented on Feb 25, 2021

    @adamsitnik
    Member

    It's non generic which means it boxes value types (keys and values)

    How is it being typically used by the end-users: to cache multiple instances of the same type or different types? If we make it generic but users end up having more cache instances we might get a memory overhead bigger from just the boxing in the non-generic version.

  4. adamsitnik commented on Feb 25, 2021

    @adamsitnik
    Member
            DateTimeOffset? AbsoluteExpiration { get; set; }
            TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
            TimeSpan? SlidingExpiration { get; set; }

    Would it be possible to make these properties have setter only, to keep the internal representation of these fields an implementation detail? So we could for example translate AbsoluteExpiration and AbsoluteExpirationRelativeToNow to ticks like StackOverflow did in their implementation

            private long _absoluteExpirationTicks;
            private readonly uint _slidingSeconds;

    We could also take @edevoogd experiment under the consideration: #45842 (comment)

  5. adamsitnik commented on Feb 25, 2021

    @adamsitnik
    Member

    The StackOverflow implementation also introduced an AccessCount field to the cache entry: #45592 (comment)

  6. adamsitnik commented on Feb 25, 2021

    @adamsitnik
    Member
  7. davidfowl commented on Feb 25, 2021

    @davidfowl
    MemberAuthor

    How is it being typically used by the end-users: to cache multiple instances of the same type or different types? If we make it generic but users end up having more cache instances we might get a memory overhead bigger from just the boxing in the non-generic version.

    Libraries end up with their own private caches. Apps usually have a single cache.

  8. AndersMad commented on Feb 27, 2021

    @AndersMad

    Wish: Pre- vs. PostEvictionCallbacks (instead of or additional)

    Short: Called before eviction with an option to skip eviction / keep the cache entry.

    Long: Skip could be:

    • A way to swap cached value with fresh data e.g. if hit count is high then refresh the data (from callback) instead of removing it and letting first user wait for new. Call should be async as IO is expected.
    • Smart extended expiration on the basis of hit count or a state of the current data etc.

    And all topic main 👍 !.. That could reduce my code removing reflection to get the main collection etc. + make cache a lot faster with this suggestion. Think the hardest part of cache is estimating the memory. Came here to clone - hope above will be a thing ~:)

  9. davidfowl commented on Mar 1, 2021

    @davidfowl
    MemberAuthor

    Short: Called before eviction with an option to skip eviction / keep the cache entry.

    Can you file an issue for this? With details and usage patterns etc.

  10. removed
    untriagedNew issue has not been triaged by the area owner
    on Mar 4, 2021
  11. added this to the Future milestone on Mar 4, 2021
  12. alastairtree commented on Mar 15, 2021

    @alastairtree

    There is a long running discussion on why I have not implemented Clear() in LazyCache over at alastairtree/LazyCache#67 which largely is due to the use of MemoryCache and it's current API. If this went ahead I suspect I would rewrite LazyCache to use MemoryCache<TKey, TValue> as David's proposal would make that much easier.

    Worth considering usage scenarios - are you suggesting one cache per item-type, or one cache per app? Having TValue in the definition would encourage me to have one cache per item-type, which makes doing Clear on all cached data (a common use case) more difficult as there are many caches. Typically up to now apps usually have one cache, with the TValue know at time of Get, but that does cause the boxing issue.

  13. davidfowl commented on Mar 15, 2021

    @davidfowl
    MemberAuthor

    I think having lots of caches is fine (we basically have this today all over the framework). It's should be treated like a dictionary with expiration.

  14. 41 remaining items

  15. ericsampson commented on Jun 26, 2021

    @ericsampson

    One thing to consider is how much should be in the runtime as default. There are several caching libraries in the .NET ecosystem (FusionCache, CacheManager, Cache Tower, EasyCaching, LazyCache, MonkeyCache, and probably a bunch of others) which can handle the more complicated and feature rich scenarios.

    That's a fair point :) If the .NET/ASP docs for caching can list these community packages, that would go a long way.

    I am biased as the creator of one of those caching libraries but my view is that it is the simple/common scenario that the MemoryCache implementation should aim for - the ConcurrentDictionary with expiration scenario. It should be fast, it should be straight forward and a lot of people should use it, it just shouldn't be all-encompassing.

    "straight forward" is an important qualifier :)
    Maybe the theoretical MemoryCache extension library could have
    GetOrAddLazy*
    alongside the current re-entrant factory version, to help discoverability in the IDE etc. Because that's the biggest current footgun for people IME. Cheers

  16. ohroy commented on Jul 23, 2021

    @ohroy

    +1
    current cache is hard to use, the cache stampede is very serious problem

  17. davidfowl commented on Jul 23, 2021

    @davidfowl
    MemberAuthor

    current cache is hard to use, the cache stampede is very serious problem

    Its possible to work around manually.

  18. jodydonetti commented on Jul 23, 2021

    @jodydonetti
    Contributor

    current cache is hard to use, the cache stampede is very serious problem

    If you'd like to avoid the cache stampede problem you can take a look at some alternatives (in alphabetical order):

    Hope this helps.

  19. ohroy commented on Jul 23, 2021

    @ohroy

    current cache is hard to use, the cache stampede is very serious problem

    Its possible to work around manually.

    sure, it can work around by lazy and lock etc, but not everyone can realize that it may have the problem of cache stampede , which will lead to serious consequences.
    whatever, it is not easy to use.
    I very hope to have a built-in solution , thank you for this proposal

  20. molinch commented on Jan 20, 2022

    @molinch

    current cache is hard to use, the cache stampede is very serious problem

    Its possible to work around manually.

    I know it's a fairly broad question where implementation may depend on uses cases, but what solution would you suggest when the cache key is dynamic ? (for example when you want to cache fetched user permissions so cache key could be user-17, user-18, ...). Having a SemaphoreSlim per key seems complicated and in the long run implies a memory leak. Relying on Lazy<T> semantics may work too depending on how the current MemoryCache is implemented.

  21. jodydonetti commented on Jan 20, 2022

    @jodydonetti
    Contributor

    current cache is hard to use, the cache stampede is very serious problem

    Its possible to work around manually.

    I know it's a fairly broad question where implementation may depend on uses cases, but what solution would you suggest when the cache key is dynamic ? (for example when you want to cache fetched user permissions so cache key could be user-17, user-18, ...). Having a SemaphoreSlim per key seems complicated and in the long run implies a memory leak. Relying on Lazy<T> semantics may work too depending on how the current MemoryCache is implemented.

    One of these #48567 (comment) ?

  22. molinch commented on Jan 21, 2022

    @molinch

    Thanks @jodydonetti, since our need is simply an IMemoryCache without cache stampede issue, nothing more, nothing less. We went with @StephenCleary solution in the end: https://gist.github.com/StephenCleary/39a2cd0aa3c705a984a4dbbea8275fe9

    I like this solution, it's a slim wrapper on top of IMemoryCache and you can easily follow the code.

  23. robertbaumann commented on Jul 17, 2022

    @robertbaumann

    Consider something that can be extensible to support Azure Caching Guidance

    • ICache<TKey, TValue>: This is the base interface that would be the dependency for code
      • Has simple Add(TKey, TValue), Remove(TKey), TValue Get(TKey) methods as well as async flavors of those methods to await retrieval from a remote source
      • Concrete implementations for ICache<TKey, TValue>, e.g. MemoryCache, RedisCache
    • MemoryCache<TKey, TValue>: Wraps a ConcurrentDictionary with expiration
      • Default expiration policy for cache entries passed in during initialization
  24. davidfowl commented on Nov 29, 2023

    @davidfowl
    MemberAuthor

    There's a proposal for a new cache implementation here dotnet/extensions#4766. Can those interested review it and leave comments?

  25. jodydonetti commented on Nov 30, 2023

    @jodydonetti
    Contributor

    Thanks David, will do!

  26. austinmfb commented on Dec 5, 2023

    @austinmfb

    Interesting that there is no mention of managing cache dependencies in this thread. Not that it would have to be baked into a new cache class, but still seems like a relevant design consideration. Is nobody really using a consistent pattern for this that they want their cache class to handle? Is everyone just opting for inline code in each application component requiring this, and managing its specific set of dependent caches?

  27. JoelDavidLang commented on Dec 7, 2023

    @JoelDavidLang

    ICacheEntry uses an unintuitive pattern for adding new entries

    I was bitten by this just recently. I did not see any documentation in CreateEntry() nor on ICacheEntry that stated that the object needed to be disposed to commit it to the cache. As a result, the code I initially wrote didn't have working caching!

    I would like to see this documentation clarified for the current version of the cache at least.

  28. locked and limited conversation to collaborators on Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions