EF Core inherited CachedRepository throws System.InvalidOperationException

36 Views Asked by At

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
0

There are 0 best solutions below