Search code examples
c#.netasynchronous.net-corememorycache

Is it possible to avoid caching when calling the extension method GetOrCreateAsync on an IMemoryCache?


I'm using an IMemoryCache to cache a token retrieved from an Identity server.

In the past I've used the GetOrCreateAsync extension method available in the Microsoft.Extensions.Caching.Abstractions library. It is very helpful because I can define the function and the expiration date at the same time.

However with a token, I won't know the expires in x amount of seconds value until the request is finished. I want to account for a use case of the value not existing by not caching the token at all.

I have tried the following

var token = await this.memoryCache.GetOrCreateAsync<string>("SomeKey", async cacheEntry =>
{
    var jwt = await GetTokenFromServer();
    var tokenHasValidExpireValue = int.TryParse(jwt.ExpiresIn, out int tokenExpirationSeconds);
    
    if (tokenHasValidExpireValue)
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(tokenExpirationSeconds);
    }
    else // Do not cache value.  Just return it.
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(0); //Exception thrown.  Value needs to be positive.
    }

    return jwt.token;
}

As you can see, an exception is thrown when I try to set an expiration of no time TimeSpan.FromSeconds(0).

Is there a way around this besides calling the Get and Set methods separately? I would like to use the GetOrCreateAsync method if possible.


Solution

  • You can't actually accomplish this with the current extension because it will always create an entry BEFORE calling the factory method. That said, you can encapsulate the behavior in your own extension in a manner which feels very similar to GetOrCreateAsync.

    public static class CustomMemoryCacheExtensions
    {
        public static async Task<TItem> GetOrCreateIfValidTimestampAsync<TItem>(
            this IMemoryCache cache, object key, Func<Task<(int, TItem)>> factory)
        {
            if (!cache.TryGetValue(key, out object result))
            {
                (int tokenExpirationSeconds, TItem factoryResult) = 
                    await factory().ConfigureAwait(false);
    
                if (tokenExpirationSeconds <= 0)
                {
                    // if the factory method did not return a positive timestamp,
                    // return the data without caching.
                    return factoryResult;
                }
    
                // since we have a valid timestamp:
                // 1. create a cache entry
                // 2. Set the result
                // 3. Set the timestamp
                using ICacheEntry entry = cache.CreateEntry(key);
    
                entry.Value = result;
                entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(tokenExpirationSeconds);
            }
    
            return (TItem)result;
        }
    }
    

    You can then call your extension method in a very similar manner:

    var memoryCache = new MemoryCache(new MemoryCacheOptions());
    var token = await memoryCache.GetOrCreateIfValidTimestampAsync<string>("SomeKey", async () =>
    {
        var jwt = await GetTokenFromServer();
        var tokenHasValidExpireValue = int.TryParse(jwt.ExpiresIn, out int tokenExpirationSeconds);
    
        return (tokenExpirationSeconds, jwt.token);
    }