Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating IMemoryCache once an entity has changed

I have a CacheService that uses GetOrCreateAsync to create cache based on a key. I am caching a photograph entity, which has a byte[] property. This caches fine and is retrieved as expected. However if the photograph entity is updated, the cache still retains the old entity as you would expect because it has not expired, how can I force an update to the cache upon save of this entity? Do I remove the existing cached entity and re-add the updated one?

Example of my FromCacheAsync method in my CacheService

    public async Task<T> FromCacheAsync<T>(string entityName, int clientId, Func<Task<T>> function)
    {
        string cacheKey = GetClientCacheKey(entityName, clientId, function);

        if (!_cache.TryGetValue(cacheKey, out T entry))
        {
            async Task<T> factory(ICacheEntry cacheEntry)
            {
                return await function();
            }
            return await _cache.GetOrCreateAsync(cacheKey, factory);
        }

        return entry;
    }

This is an example of using the caching.

      var existingPhotograph = await _cacheService.FromCacheAsync(nameof(_context.Photograph), clientId, async () =>
            await _photographRepository.GetByStaffIdAsync(staff.StaffId));
like image 800
user2270653 Avatar asked Jan 28 '23 16:01

user2270653


1 Answers

You need to invalidate the cache key, when the entity changes.

That may be a bit tricky, if you directly operate on the DbContext. But since you are using repository pattern, that`s easier to do.

It boils down to inject the IMemoryCache into your repository and invalidate it when a picture is updated.

public class PhotographRepository : IPhotograpRepository
{
    private readonly IMemoryCache _cache;
    public PhotographReposiory(IMemoryCache cache, ...)
    {
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
    }

    public async Task Update(PhotographEntity entity)
    {
        // update your entity here
        await _context.SaveChangesAsync();

        // this invalidates your memory cache. Next call to _cache.TryGetValue
        // results in a cache miss and the new entity is fetched from the database
        _cache.Remove(GetClientCacheKey(entityName, clientId));
    }
}

Using with Decorator pattern

public class PhotographRepository : IPhotograpRepository
{
    private readonly ApplicationDbContext _context;
    public PhotographReposiory(ApplicationDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task Update(PhotographEntity entity)
    {
        // update your entity here
        await _context.SaveChangesAsync();
    }
}


public class CachedPhotographRepository : IPhotograpRepository
{
    private readonly IMemoryCache _cache;
    private readonly IPhotograpRepository _repository;
    public CachedPhotographRepository(IPhotograpRepository repository, IMemoryCache cache)
    {
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
        _repository = _repository ?? throw new ArgumentNullException(nameof(repository));
    }

    public async Task Update(PhotographEntity entity)
    {
        // do the update in the passed in repository
        await _repository.Update(entity);

        // if no exception is thrown, it was successful
        _cache.Remove(GetClientCacheKey(entityName, clientId));
    }
}

The catch is, the built-in DI/IoC container doesn't support decorator registrations, so you'll have to make it yourself via factory pattern or use a 3rd party IoC container which supports it.

services.AddScoped<IPhotograpRepository>(provider =>
    // Create an instance of PhotographRepository and inject the memory cache
    new CachedPhotographRepository(
        // create an instance of the repository and resolve the DbContext and pass to it
        new PhotographRepository(provider.GetRequiredService<ApplicationDbContext>()),
        provider.GetRequiredService<IMemoryCache>()
    )
);

It's per se not "bad" to use new within the composition root (where you configure your DI/IoC container), but with 3rd party IoC container its just more convenient.

Of course you can also register PhotographRepository with the IoC container and have it resolved. But that would also allow you to inject PhotographRepository into your services whereas the above prevents it, because only the IPhotographRepository interface is registered.

like image 76
Tseng Avatar answered Feb 05 '23 05:02

Tseng