I have Service that makes use of a service/repository-pattern. Some but not all repositories inherit from a CachedRepository. Since I have a lot of repositories that make use of caching I did not want to go with a decorator pattern. The cache works fine. But when it handles more than one request then more often then not it will throw this exception:
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
All the repositories and the services that make use of them are dependency injected with a Scoped ServiceLifetime. A repository might be used by multiple services.
public class CachedRepository
{
private readonly IMemoryCache cache;
const int expirationTime = 20;
public CachedRepository(IMemoryCache cache)
{
this.cache = cache;
}
protected async Task<T> GetCacheItem<T>(Task<T> fetchData, string key, int expirationMinutes = expirationTime)
{
string cacheKey = $"{key}";
if (cache.TryGetValue(cacheKey, out T cacheItem))
{
return cacheItem;
}
cacheItem = await fetchData;
var now = DateTimeOffset.UtcNow;
cache.Set(cacheKey, cacheItem, now.AddMinutes(expirationMinutes));
return cacheItem;
}
}
public class SomeEntityRepository : CachedRepository
{
private readonly SomeContext context;
private readonly string repoName;
public SomeEntityRepository (SomeContext context, IMemoryCache cache) : base(cache)
{
this.context = context;
this.repoName = GetType().FullName;
}
public async Task<SomeEntity> GetSomeEntity(int id)
{
const string methodName = nameof(GetSomeEntity);
string cacheKey = $"{repoName}_{methodName}_{id.ToString()}";
Task<SomeEntity> func(int i) => context.SomeEntities
.AsNoTracking()
.Where(x=> x.Id == i)
.FirstOrDefaultAsync();
return await GetCacheItem(func(id), cacheKey);
}
}
I've tried the following:
- Adding locks/semaphores/mutexes to the cached repository before fetchdata and free ressource after.
- Changed Task in method to be async/await.
- Changed Task in method to be a Func<Task>>
- Set query tracking behavior on context to be NoTracking as default
- Set ServiceLifetime on context to be Transient
- Triple checked that await is used everywhere the context is accessed