Search code examples
c#.netcaching

.NET 8: MemoryCache SlidingExpiration without accessing


My question is similair to this one, but the answers only explain why things happen the way they do, not how to actually achieve the desired solution.

Right now, when defining

var options = new MemoryCacheEntryOptions()
{
    // After x minutes of non-access, remove
    SlidingExpiration = TimeSpan.FromMinutes(x),
}
.RegisterPostEvictionCallback(OnCacheItemRemoved);

A cacheItem will only be removed when accessing it on a timepoint t1 > t0 + x'. That means that if I do not access the item anymore after that point, it also will never be removed from cache.

I could set an absolute expiration yes, but that would overrule the sliding window. If I wanted it to stay in memory if it still being accessed, then it would be evicted due to the aboslute expiration. What I want is a simple sliding window with automatic eviction once the item hasn't been accessed in x minutes. I'd expect an internal timer to handle this. It does not need to run every second for performance reasons, once a minute is fine enough for me.

I do not see any configuration for how to check for eviction either.

EDIT When adding the MemoryCache for DI, there is in fact an option for ExpirationScanFrequency

services.AddMemoryCache(options => {
    // Check for expired cache items once a minute
    options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
});

but this does not seem to work. My callback still isn't being invoked several minutes later even though nothing accesses it. X is set to 1 for now.


Solution

  • Below is a code sample that starts a timer to check for expired items, which may suite your needs.

    Items will be expired from the cache every 5 seconds -- you can change the length of time by changing the two lines:

    cache_opts.ExpirationScanFrequency = TimeSpan.FromSeconds( 5 );
    
    cache_check_timer = new System.Timers.Timer( 5000 );
    

    In the sample, a memory cache is created, and 3 items are added, each of which will expire if not accessed within the sliding expiration timespan:

    using Microsoft.Extensions.Caching.Memory;
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Timers;
    
    namespace InMemoryNetCore
    {
    
       class Program
       {
    
          private static System.Timers.Timer? cache_check_timer;
    
          private static IMemoryCache? cache;
    
          public static void Main( string[] args )
          {
    
             String in_str;
    
    
             // Create a memory cache ...
             // Important - set the minimum length of time between successive scans for expired items (default is one minute) ...
    
             var cache_opts = new MemoryCacheOptions();
    
             cache_opts.ExpirationScanFrequency = TimeSpan.FromSeconds( 5 );
    
             cache = new MemoryCache( cache_opts );
    
    
             // Create a timer with a five second interval ...
    
             cache_check_timer = new System.Timers.Timer( 5000 );
    
             cache_check_timer.AutoReset = true;
    
             cache_check_timer.Elapsed += cache_check;
    
             cache_check_timer.Start();
    
    
             // Create cache item which executes call back function
    
             var cacheEntryOptions = new MemoryCacheEntryOptions()
                 .SetPriority( Microsoft.Extensions.Caching.Memory.CacheItemPriority.Normal )
                 .SetSlidingExpiration( TimeSpan.FromSeconds( 5 ) )
                 .RegisterPostEvictionCallback( callback: cache_item_removed );
    
    
             // add cache Item with options of callback
    
             cache.Set( 1, "Cache Item 1", cacheEntryOptions );
    
             cache.Set( 2, "Cache Item 2", cacheEntryOptions );
    
             cache.Set( 3, "Cache Item 3", cacheEntryOptions );
    
    
             // Get user input to terminate program ...
    
             in_str = String.Empty;
    
             while ( !"QUIT".Equals( in_str ) )
             {
    
                Console.Write( "Enter QUIT to end: " );
    
                in_str = Console.ReadLine();
    
             }
    
    
             // Stop and dispose of timer ...
    
             cache_check_timer.Stop();
             
             cache_check_timer.Dispose();
    
    
             Console.WriteLine( "\nProgram ended" );
    
          }
    
          // Used to issue a Get request to the cache using a key value that we
          // know does not exist - only to invoke the internal scan for expired items ...
          private static void cache_check( object? sender, ElapsedEventArgs e )
          {
    
             //Console.WriteLine( "Here in cache_check" );
             //Console.WriteLine( "Timer event was raised at {0:HH:mm:ss.fff}", e.SignalTime );
    
             if ( cache is not null )
             {
    
                // Issue a Get for a key that does not exist ...
    
               _ = cache.Get( -1 );
    
             }
    
          }
    
    
          // Callback when cached items are removed ...
          private static void cache_item_removed( object key, object? value, EvictionReason reason, object? state )
          {
    
             Console.Write( "\nCache key: " + key + " removed from cache due to: " + reason );
    
          }
    
       }
    
    }