I am attempting to create a BaseService that uses Redis cache to build out the following pattern:
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);
}
}
}
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 .
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.