We practice CQRS architecture in our application, i.e. we have a number of classes implementing ICommand
and there are handlers for each command: ICommandHandler<ICommand>
. Same way goes for data retrieval - we have IQUery<TResult>
with IQueryHandler<IQuery, TResult>
. Pretty common these days.
Some queries are used very often (for multiple drop downs on pages) and it makes sense to cache the result of their execution. So we have a decorator around IQueryHandler that caches some query executions.
Queries implement interface ICachedQuery
and decorator caches the results. Like this:
public interface ICachedQuery {
String CacheKey { get; }
int CacheDurationMinutes { get; }
}
public class CachedQueryHandlerDecorator<TQuery, TResult>
: IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
private IQueryHandler<TQuery, TResult> decorated;
private readonly ICacheProvider cacheProvider;
public CachedQueryHandlerDecorator(IQueryHandler<TQuery, TResult> decorated,
ICacheProvider cacheProvider) {
this.decorated = decorated;
this.cacheProvider = cacheProvider;
}
public TResult Handle(TQuery query) {
var cachedQuery = query as ICachedQuery;
if (cachedQuery == null)
return decorated.Handle(query);
var cachedResult = (TResult)cacheProvider.Get(cachedQuery.CacheKey);
if (cachedResult == null)
{
cachedResult = decorated.Handle(query);
cacheProvider.Set(cachedQuery.CacheKey, cachedResult,
cachedQuery.CacheDurationMinutes);
}
return cachedResult;
}
}
There was a debate whether we should have an interface on queries or an attribute. Interface is currently used because you can programmatically change the cache key depending on what is being cached. I.e. you can add entities' id into cache key (i.e. have keys like "person_55", "person_56", etc.).
The issue is of course with cache invalidation (naming and cache invalidation, eh?). Problem with that is that queries do not match one-to-one with commands or entities. And execution of a single command (i.e modification of a person record) should render invalid multiple cache records: person record and drop down with persons' names.
At the moment I have a several candidates for the solution:
ICacheRelated
and return all these keys as part of this interface. And when EntityFramework is updating/creating the record, get these cache keys and invalidate them. (Hacky!)ICacheInvalidatingCommand
that should return list of cache keys that should be invalidated. And have a decorator on ICommandHandler
that will invalidate the cache when the command is executed.I don't like any of the options (maybe apart from number 4). But I think that option 2 is one I'll give a go. Problem with this, cache key generation becomes messy, I'll need to have a common place between commands and queries that know how to generate keys. Another issue would that it'll be too easy to add another cached query and miss the invalidating part on commands (or not all commands that should invalidate will invalidate).
Any better suggestions?
Cache invalidation refers to process during which web cache proxies declare cached content as invalid, meaning it will not longer be served as the most current piece of content when it is requested. Several invalidation methods are possible, including purging, refreshing and banning.
Cache invalidation is a process where the computer system declares the cache entries as invalid and removes or replaces them. The basic objective of using cache invalidation is that when the client requests the affected content, the latest version is returned.
By definition, a cache doesn't hold the source of truth of your data (e.g., a database). Cache invalidation describes the process of actively invalidating stale cache entries when data in the source of truth mutates.
I'm wondering whether you should really do caching here at all, since SQL server is pretty good in caching results, so you should see queries that return a fixed list of drop down values to be really fast.
Of course, when you do caching, it depends on the data what the cache duration should be. It depends on how the system is used. For instance, if new values are added by an administrator, it's easy to explain that it takes a few minutes before other users will see his changes.
If, on the other hand, a normal user is expected to add values, while working with a screen that has such list, things might be different. But in that case, it might even be good to make the experience for the user more fluent, by presenting him with the drop down or giving him the option to add a new value right there. That new value is than processed in the same transaction and everything will be fine.
If you want to do cache invalidation however, I would say you need to let your commands publish domain events. This way other independent parts of the system can react to this operation and can do (among other things) the cache invalidation.
For instance:
public class AddCityCommandHandler : ICommandHandler<AddCityCommand>
{
private readonly IRepository<City> cityRepository;
private readonly IGuidProvider guidProvider;
private readonly IDomainEventPublisher eventPublisher;
public AddCountryCommandHandler(IRepository<City> cityRepository,
IGuidProvider guidProvider, IDomainEventPublisher eventPublisher) { ... }
public void Handle(AddCityCommand command)
{
City city = cityRepository.Create();
city.Id = this.guidProvider.NewGuid();
city.CountryId = command.CountryId;
this.eventPublisher.Publish(new CityAdded(city.Id));
}
}
Here you publish the CityAdded
event which might look like this:
public class CityAdded : IDomainEvent
{
public readonly Guid CityId;
public CityAdded (Guid cityId) {
if (cityId == Guid.Empty) throw new ArgumentException();
this.CityId = cityId;
}
}
Now you can have zero or more subscribers for this event:
public class InvalidateGetCitiesByCountryQueryCache : IEventHandler<CityAdded>
{
private readonly IQueryCache queryCache;
private readonly IRepository<City> cityRepository;
public InvalidateGetCitiesByCountryQueryCache(...) { ... }
public void Handle(CityAdded e)
{
Guid countryId = this.cityRepository.GetById(e.CityId).CountryId;
this.queryCache.Invalidate(new GetCitiesByCountryQuery(countryId));
}
}
Here we have special event handler that handles the CityAdded
domain event just to invalide the cache for the GetCitiesByCountryQuery
. The IQueryCache
here is an abstraction specially crafted for caching and invalidating query results. The InvalidateGetCitiesByCountryQueryCache
explicitly creates the query who's results should be invalided. This Invalidate
method can than make use of the ICachedQuery
interface to determine its key and invalide the results (if any).
Instead of using the ICachedQuery
to determine the key however, I just serialize the whole query to JSON and use that as key. This way each query with unique parameters will automatically get its own key and cache, and you don't have to implement this on the query itself. This is a very safe mechanism. However, in case your cache should survive AppDomain recycles, you need to make sure that you get exactly the same key across app restarts (which means the ordering of the serialized properties must be guaranteed).
One thing you must keep in mind though is that this mechanism is especially suited in case of eventual consistency. To take the previous example, when do you want to invalidate the cache? Before you added the city or after? If you invalidate the cache just before, it's possible that the cache is repopulated before you do the commit. That would suck of course. On the other hand, if you do it just after, it's possible that someone still observes the old value directly after. Especially when your events are queued and processed in the background.
But what you can do is execute the queued events directly after you did the commit. You can use a command handler decorator for that:
public class EventProcessorCommandHandlerDecorator<T> : ICommandHandler<T>
{
private readonly EventPublisherImpl eventPublisher;
private readonly IEventProcessor eventProcessor;
private readonly ICommandHandler<T> decoratee;
public void Handle(T command)
{
this.decotatee.Handle(command);
foreach (IDomainEvent e in this.eventPublisher.GetQueuedEvents())
{
this.eventProcessor.Process(e);
}
}
}
Here the decorator depends directly on the event publisher implementation to allow calling the GetQueuedEvents()
method that would be unavailable from the IDomainEventPublisher
interface. And we iterate all events and pass those events on to the IEventProcessor
mediator (which just works as the IQueryProcessor
does).
Do note a few things about this implementation though. It's NOT transactional. If you need to be sure that all your events get processed, you need to store them in a transactional queue and process them from there. For cache invalidation however, it doesn't seem like a big problem to me.
This design might seem like overkill just for caching, but once you started publishing domain events, you'll start to see many use cases for them that will make working with your system considerably simpler.
Are you using a separate read and write model? If so, perhaps your "projection" classes (the ones that handle events from the write model and do CRUD on the read model) could invalidate the appropriate cache entries at the same time.
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