INITIALIZING
c#

In memory cache and eviction with C#

In memory cache and eviction with C#

In-memory cache is a fast, easy way to serve up data — objects, lists, whatever — that gets hammered constantly. Not everything belongs in memory, and Redis will get you the same result for plenty of use cases, minus the part where a bad deploy wipes your entire cache into oblivion. Just remember: memory might feel “infinite” in the cloud, right up until you see the bill, at which point it becomes extremely finite, extremely fast.

Disclaimer delivered. In-memory cache is still a life saver, and I will die on this hill.

Personally, I reach for it with arrays, lists, certificates, hash tables, and anything I know gets shared across sessions or hit constantly by the application — the stuff that, if you had to fetch it fresh every single time, would make your API feel like it's running on a potato. As a rule of thumb, expire the cache at least once a week — memory that never clears is just a slow leak wearing a caching costume and hoping nobody notices. Below, we'll build a generic class that takes a delegate to populate the cache with whatever type you throw at it, no questions asked.

Date Time Helper

First, a helper class to turn a CRON expression into an actual timespan, because apparently telling a computer “run this every Sunday at 11pm” requires a small incantation.

CRON expressions are genuinely great, and I genuinely do not remember their syntax, same as regex — my brain has apparently allocated exactly one slot for “cryptic mini-language nobody can read at a glance,” and regex claimed it years ago and is refusing to share custody. These days I just ask an AI to write the expression for me, which feels like cheating right up until it saves twenty minutes of squinting at asterisks and question marks like they personally wronged me.

Microsoft Copilot handles this fine for free, no PhD in astrology required:

Prompt: create a cron expression that executes every Sunday at 11 pm

Result: 0 23 * * 0

With the expression in hand, the helper class does the actual work using Cronos — a .NET library that parses CRON expressions properly, including time zones, which matters more than it sounds like it should the first time your server and your cache mysteriously disagree about what time it is and you lose an hour of your life to a UTC-related feud:

/// <summary>
/// Date Time Helper class.
/// </summary>
public static class DateTimeHelper
{
    /// <summary>
    /// Rounds the specified DateTime to the nearest minute.
    /// </summary>
    public static DateTime RoundToNearestMinute(this DateTime dt, int minutesToRound, RoundingDirection direction)
    {
        if (minutesToRound == 0) //can be > 60 mins
            return dt;

        var d = TimeSpan.FromMinutes(minutesToRound); //this can be passed as a parameter, or use any timespan unit FromDays, FromHours, etc.

        long delta = 0;
        var modTicks = dt.Ticks % d.Ticks;

        switch (direction)
        {
            case RoundingDirection.Up:
                delta = (modTicks != 0) ? d.Ticks - modTicks : 0;
                break;

            case RoundingDirection.Down:
                delta = -modTicks;
                break;

            case RoundingDirection.Nearest:
                {
                    bool roundUp = modTicks > (d.Ticks / 2);
                    var offset = roundUp ? d.Ticks : 0;
                    delta = offset - modTicks;
                    break;
                }
        }
        return new DateTime(dt.Ticks + delta, dt.Kind);
    }

    /// <summary>
    /// Calculates the TimeSpan relative to the current time based on the given cron expression.
    /// </summary>
    public static TimeSpan TimeSpanRelativeToNow(string cronExpression, string timeZone = "Pacific Standard Time")
    {
        var nearest = DateTime.Now.RoundToNearestMinute(1, RoundingDirection.Down);
        var nearestOccurrence = DateTimeOffset.Parse(nearest.ToString());
        var expression = CronExpression.Parse(cronExpression);
        var nextOccurrence = expression.GetNextOccurrence(nearestOccurrence, TimeZoneInfo.FindSystemTimeZoneById(timeZone), inclusive: false);

        if (nextOccurrence.HasValue)
        {
            var next = nextOccurrence.Value.ToLocalTime().DateTime;
            return next - nearest;
        }
        else
        {
            throw new InvalidOperationException("No next occurrence found for the given cron expression.");
        }
    }
}

/// <summary>
/// Rounding direction.
/// </summary>
public enum RoundingDirection
{
    Up,
    Down,
    Nearest
}

Two methods worth knowing about: rounding a DateTime up, down, or to the nearest minute (not actually used in this example, but it's there when you need it), and calculating the timespan until the next CRON occurrence — properly time-zone-aware, since your hosting provider's clock and your intended schedule are not contractually obligated to be on speaking terms.

Memory Class

The in-memory class itself takes a generic return type — ideally something serializable, though nothing stops you from bolting on a guard clause if you want to enforce that with extreme prejudice.

Since this needs to be thread-safe, it leans on a semaphore per key rather than one giant lock for everything, so unrelated cache keys aren't standing in line behind each other for no reason like it's the DMV:

/// <summary>
/// Represents a memory cache for storing and retrieving items.
/// </summary>
/// <typeparam name="TItem">The type of the items stored in the cache.</typeparam>
public class Memory<TItem>
{
    private readonly MemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());
    private readonly ConcurrentDictionary<object, SemaphoreSlim> _locks = new();

    /// <summary>
    /// Gets the item from the cache with the specified key. If the item does not exist in the cache, it will be created using the provided delegate function.
    /// </summary>
    public async Task<TItem> GetOrCreate(object key, Func<Task<TItem>> createItem, TItem? cacheEntry, string cronExpression = "none", string timeZone = "Pacific Standard Time")
    {
        if (!_memoryCache.TryGetValue(key, out cacheEntry)) // Look for cache key.
        {
            SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
            // Notify all threads trying to access this value to await creation
            await mylock.WaitAsync();
            try
            {
                if (!_memoryCache.TryGetValue(key, out cacheEntry))
                {
                    // Key not in cache, so get data, this is a delegate function
                    cacheEntry = await createItem();

                    if (cronExpression == "none")
                        _memoryCache.Set(key, cacheEntry);
                    else
                    {
                        // Obtain absolute expiration relative to now in time span format
                        var timeSpan = DateTimeHelper.TimeSpanRelativeToNow(cronExpression, timeZone);
                        _memoryCache.Set(key, cacheEntry, timeSpan);
                    }
                }
            }
            finally
            {
                mylock.Release();
            }
        }

        // Ensure cacheEntry is not null before returning
        return cacheEntry ?? throw new InvalidOperationException("Cache entry creation failed.");
    }

    /// <summary>
    /// Deletes the cache entry with the specified key.
    /// </summary>
    public async Task<bool> DeleteEntry(object key)
    {
        SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
        await mylock.WaitAsync();
        try
        {
            _memoryCache.Remove(key);
        }
        finally
        {
            mylock.Release();
        }

        return true;
    }
}

A delegate handles actually producing the value once the real work is done, and gets stored in memory afterward, no further supervision required. Last step: check whether a CRON expression was supplied. “none” (the default) means the item never expires, living forever like a rumor; anything else gets run through the date-time helper to figure out exactly when this entry should quietly, politely disappear.

Usage

Fair question: why a delegate at all? Why not just... put the value in?

Because I want item-creation logic living somewhere else entirely, fully decoupled from the caching mechanism itself. If I need to fetch a certificate from Azure Key Vault, that's an Azure Key Vault concern, full stop — the cache shouldn't need to know or care how the value was produced, any more than a vending machine needs to know how the soda company bottled its product. It just needs to know there's a slot, and eventually, a can.

Beyond certificates, I reach for this pattern for:

  • API responses
  • LINQ-to-IEnumerable results
  • Dictionaries

This example just caches a string, but the idea scales to anything you're tired of recomputing:

static async Task Main(string[] args)
{
    // Create an instance of Memory<TItem> with the desired type
    var memory = new MemoryCacheLibrary.Memory<string>();

    // Define the key for the cache entry
    object key = "myCacheKey";

    // Define the delegate function to create the item if it does not exist in the cache
    Func<Task<string>> createItem = async () =>
    {
        // Simulate some time-consuming operation to create the item
        await Task.Delay(1000);
        return "Cached Item";
    };

    // Define the cron expression for cache expiration
    string cronExpression = "0 0 * * *"; // Run every day at midnight

    // Get or create the item from the cache with the specified key and cron expression
    string cacheEntry = await memory.GetOrCreate(key, createItem, default, cronExpression: cronExpression, timeZone: "Pacific Standard Time");

    // Print the cache entry
    Console.WriteLine(cacheEntry);

    // Delete the cache entry with the specified key
    await memory.DeleteEntry(key);
}

Use a singleton (or a static instance) for the in-memory class — that's what guarantees exactly one instance, living for the entire lifetime of the application, instead of a fresh empty cache quietly spawning every time someone asks for one and wondering why “caching” never seems to cache anything:

//declaration
builder.Services.AddSingleton((c) => {
    return new MemoryCacheLibrary.Memory<string>();
});

//injection
public class SomeClass
{
    private readonly Memory<string> _inMemory;

    public SomeClass(Memory<string> inMemory)
    {
        _inMemory = inMemory;
    }

    public async Task<bool> DoSomething()
    {
        object key = "myCacheKey";

        Func<Task<string>> createItem = async () =>
        {
            // Simulate some time-consuming operation to create the item
            await Task.Delay(1000);
            return "Cached Item";
        };

        // Define the cron expression for cache expiration
        string cronExpression = "0 0 * * *"; // Run every day at midnight

        string cacheEntry = await _inMemory.GetOrCreate(key, createItem, default, cronExpression: cronExpression, timeZone: "Pacific Standard Time");

        return true;
    }
}

Happy coding!!!

Para valorar camisetas de clubes para mujer con criterios claros, es recomendable revisar la versión local, visitante o alternativa. Para evitar errores, merece la pena revisar que las imágenes muestren frontal, espalda, escudo y cuello.