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));
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));
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With