0

I am attempting to create a BaseService that uses Redis cache to build out the following pattern:

  1. Get from cache
  2. If in cache, return result
  3. If result is null, call Func to get result from source (database)
  4. Place in cache
  5. Return result from Func

I have everything working, but for some reason the service method that is calling to get the result is needing an "await await" before it will compile. I can't seem to figure out why my ResultFromCache method which is meant to imitate what Ok() does in WebAPI is doubly wrapped. Can you please help me find where I am not returning the correct result so my users of this pattern won't have to use two awaits to get their results :)

Here's a slimmed down version of my code that requires the await await in the GetMessage method.

using StackExchange.Redis;
using System.Text.Json;

namespace TestCache
{
    public class Service: BaseService
    {
        //Injected DbContextx
        private Context context { get; }

        //service method
        public async Task<Message> GetMessage(int accountId)
        {
            return await await ResultFromCache(accountId, FetchMessage, "Message");
        }

        //get from database method
        private async Task<Message> FetchMessage(int parentId)
        {
            //Example of using EF to retrieve one record from Message table
            return await context.Message;
        }
    }

    public class BaseService
    {
        private const int Hour = 3600;

        private ConnectionMultiplexer connection;
        private IDatabaseAsync database;

        public async Task<T1> ResultFromCache<T1, T2>(T2 param1, Func<T2, T1> fromSource, string cacheKey, int cacheDuration = Hour)
        {
            //get from cache
            var result = await CacheGet<T1>(cacheKey);

            if (result != null)
                return result;

            //get from db
            result = fromSource(param1);

            //TODO: add to cache
            return result;
        }

        public async Task<T> CacheGet<T>(string key)
        {
            var value = await database.StringGetAsync(key);

            if (value.IsNull)
            {
                return default;
            }

            return JsonSerializer.Deserialize<T>(value);
        }
    }
}
RichardAZ
  • 3
  • 4
  • You've got a number of issues in the above code, but the largest one is that `fromSource` should actually be defined as `Func> fromSource` and then awaited properly in your ResultFromCache method. – David L Jan 31 '23 at 18:50
  • @DavidL: Thank you for the tip, that fixed my problem. I would love to learn more issues in the code block so I can learn a better way to do what I'm trying to do :) – RichardAZ Jan 31 '23 at 21:35

1 Answers1

0

As I mentioned in my comment, your fromSource needs to be defined as Func<T2, Task<T1>, since you need to await the func.

However, you also have a subtle bug in regards to checking for null in ResultFromCache. As it is currently written, value types will incorrectly return if the default is returned in CacheGet<T>. To solve for this, you need to use `EqualityComparer.Default.Equals to check for the default value instead of simply null.

public class BaseService
{
    private const int Hour = 3600;

    private ConnectionMultiplexer connection;
    private IDatabaseAsync database;

    public async Task<T1> ResultFromCache<T1, T2>(T2 param1, Func<T2, Task<T1>> fromSource, 
        string cacheKey, int cacheDuration = Hour)
    {
        //get from cache
        var result = await CacheGet<T1>(cacheKey);

        if (!EqualityComparer<T1>.Default.Equals(result, default(T1)))
            return result;

        //get from db
        result = await fromSource(param1);

        //TODO: add to cache
        return result;
    }

    public async Task<T> CacheGet<T>(string key)
    {
        var value = await database.StringGetAsync(key);

        if (value.IsNull)
        {
            return default;
        }

        return JsonSerializer.Deserialize<T>(value);
    }
}

Finally, if multiple requests experience Redis cache misses at the same time, they will all call your loader function. This can lead to significant strain on your loading source if the queries are expensive. A common solution is double-checked locking which can be implemented as a ConcurrentDictionary of SemaphoreSlim instances, or a much better implementation in Stephen Cleary's AsyncDuplicateLock: Asynchronous locking based on a key.

David L
  • 32,885
  • 8
  • 62
  • 93