In our project we implemented caching and invalidation as attributes on controller endpoints. Now we can add caching/invalidating in declarative way (It uses controller name and endpoint parameters as key value, when invalidate it removes everything related to specified controller). I'm wondering why other don't do the same because it's handy use and you cannot make a mistake when use caching in declarative way.
I think Caching is an infrastructure concern, not an application concern. So, having the handlers invalidate the cache seems wrong. Also, having handlers that respond to domain/internal events seems wrong too. I would consider the cache a form of persistence. So, I think the persistence infra code should deal with caching data. Now if we are talking about output caching, that is a presentation layer concern, and it should probably be the presentation layer that deals with invalidating the output cache. But, sometimes being pragmatic is better than being dogmatic.
What do you mean by "having handlers that respond to domain/internal events seems wrong too"? What's going to handle the events then? Most of this can just as easily be moved to the Infrastructure layer.
Hey, Milan! Thanks for the caching related vidoes, great stuff. However, how would u go with invalidation if there were connections between resources? Example: one Order -> many OrderItems. You would need to invalidate 1) OrderItem by id 2) OrderItems filtering 3) Order that comes with his items. I suppose, you would you still go with the events approach?
Hello Milan. What happens when you have more processes and some of them are changing DB and some of them are reading from cash at the same time from more API instances? I believe that concurrency issues may happen there. What to do to prevent that and have valid results all the time?
How about we keep cache invalidation operation atomic. In redis we can use Lua scripts/watch commands while invalidating cache - during this process other redis resources would freeze. However, our app instances which are reading (not updating) cache, will face delayed response during the process.
There will always be the case for discrepancy between the source of truth and the cache. That's one thing you have to be okay with if you want to use caching.
I think caching lends itself better to the infrastructure layer, behing repository walls rather than just outside because a repository has explicit knowledge of when an entity is updated. Just a hunch. Havnt needed caching on that level so far. :)
This is generally how I do it as well. Making use of decorator pattern over a repository to make it a cached repository, any calls to update/delete/create will invalidate cache.
How would you handle caching and invalidating paginated data in a single and multi-tenant scenarios? How do you decide to choose application caching over output response caching?
but if we want to use redis as a layer between API and Database , what we should do? i mean persist data to disk and use redis as a write and read database(Cache) that after write data into redis, the data sent to an sql database.
At 12:05, how do you invalidate a specific object from the cache without it's ID being used ? From what i read, the whole "products" caching is wiped in here, no id passed ? Maybe i haven't understood something ? Thanks !
Nice content! Do we really need 3 Handlers for Cache Invalidation here? Can't we just publish the event with the cache key and use that key in the Handler to remove it from Cache?
In your expensely project. You were caching the results of query which are implementing the ICacheableQuery. So what if I create a interface named ICacheInvalidatorCommand and write a function that returns the cache key/keys and invalidate those keys inside of a behaviour if the response is successful.
Would love to see a more in depth cache invalidation video that covers different cache invalidation strategies, because this seems a bit simplistic and flawed. If products are changed often, then the caching would be constantly invalidated and might be considered pointless. I wonder if there's a better approach?
@MilanJovanovicTech i imagine time based would also be acceptable if the business was okay with stale data for a certain period in order to have better performance. Alrho that probably only addresses the add/update scenario...since delete scenario could cause system errors if they try to open a deleted item from a cached list somewhere.
@@MilanJovanovicTech i dont think that one will get much attention but if you have spare time definitely haha maybe 5 minutes about tricks of naming, importance, general patterns, etc
Is it a good idea to override SaveChanges method of DbContext, and invalidate cache there?(key can be typeof(Entity).Name etc.) Or is your solution more reliable? What do you think?
I use this approach (overridding SaveChangensAsync or other UoW) and it works perfect for me. One place to deal with cache invalidation and forget about it. In DbContext there is access to ChangeTracker, so clearing "product-{id}" is no problem... I wondering why Milan didn't mension about that - is it something wrong with this solution, Milan? 🤔
One unrelated thing I've been wandering about is the cancellation token. When having steps like save saving to database and then pushing an event, wouldn't cancellation possibly lead to inconsistency? Specifically in case the request is cancelled after saving to database and before pushing the event.
Very nice and efficient way Milan! Thanks for sharing😊 Recently I applied a solution for this specific problem. My solution was the approach of Pipeline Behavior which MediatR gives us out of the box. Something like `CacheInvalidationBehavior` for specific commands (Create, Update, Delete) was enough for me. Do you think which one is more usable in this case? Pipeline Behavior or Notification Handler approach 🤔
@@MilanJovanovicTech Well, You can mark your commands with an interface, like `ICacheInvalidationRequest` and inside your behavior, you can check if the commands which hit the behavior implements that interface or not. If it's includes, you can invalidate your cache. About the cache key, you can put a method inside of `ICacheInvalidationRequest` like `string GetCacheKey()` and the commands which implements this interface must implement this method also. So you can set cache key in there and use it in your `CacheInvalidationBehavior` class. I guess it's harder to say than to do 😅
You didn’t really use the IPipelineBehavior for this. Instead you used Inotification which I guess also works. But do you have an example for using the pipeline behavior or this was it?
I don't have a pipeline behavior example. You could do something with an ICacheableComand interface + some way to tell the pipeline behavior which cache key to remove.
I don't think, it is a good idea to pass the CancellationToken to the methods after SaveChanges(cancellationToken). Since you're not using a Outbox pattern, all methods that are executed after SaveChanges can be canceled and will also lead into inconsistent state, aren't they?
I've recently fix a bug of cache invalidation. the key was dinamically generated using the name of the method and the value of the parameters and in one method to add an item the invalidation of the cache was called with the wrong list of parameters... Do you have naming convention for the cache key ? and also in case mediator pattern is not used what could be a way to centralize the invalidationof the key ?
Hi , In my project I am caching Labs data (Not change very frequent) with related relationship properties ,do you think its a good idea to cache related info also ? because everytime any lab changes its relationship property like (Working Days , Documents etc.) I need to reset the cache . Mostly these related property will have count less than 10. Example : caching labs with related properties. public async Task GetAll(bool trackChanges = false) { return await FindAll(trackChanges) .Include(x => x.WorkingDays) .Include(x => x.Documents) .Include(x => x.MediaList) .Include(x => x.ServiceablePincodes) .Include(x => x.Certificates) .ToListAsync(); }
Our hardest problem to solve is cache of an object, and then someone else requests a list of the same object type. The cache is in redis and the data is in EF Core. On the surface it seems simple to look in the cache first and then get the rest. But the problem is row level (i.e. object level) security which then has to merge results but do so by applying security criteria. RavenDb largely solves this for us because it works like Redis itself when given enough memory, and thus our row level filtering queries intelligently use the cache whenever possible.
@@MilanJovanovicTech absolutely but for those not using it to cheat the issue, providing guidance on how to handling the segmented nature and ability to know if you got all of the rows relevant from cache or if you have to go back to the database is a big complex deal that almost every developer at scale ends up dealing with. (And security at row level hits us all and has to be done intelligently with caching involved. )
@@MilanJovanovicTech Good point. its a better approach. but can we do something about this? I mean, the fact that we wrote that code in multiple places.
I agree that you shouldn't prematurely add caching, your code should work well enough without it. Often it's introduced early and developers think it's a must have. Your 10 users won't blow up the database, I can assure you.
I expected you'll speak about the real cache (wich is a really interesting topic) : cache, in computer science, is hardware L1, L2, L3 cache... so, be precise ! "Database cache" is what you are speaking about.
@@MilanJovanovicTech No. You cannot say : cache implicitly mean "software data cache" : it is simply false, for me. The fact is that your softwares are ALL TIME using hardware cache - without harware memory cache, your database cache may be 2-3 level of magnitude slower. In a point of view of an experienced developper that start programming in 1990 and have made video games, there is no question about the signification of "cache" in computer science : at any time I'm doing software, I'm massivelly using hardware memory cache. So, if you say "I will discuss about cache" - it does not mean "database cache". "Database cache" is a high level strategy rellying on hardware caching systems...
Want to master Clean Architecture? Go here: bit.ly/3PupkOJ
Want to unlock Modular Monoliths? Go here: bit.ly/3SXlzSt
There are two hard things in programming.
Cache invalidation, naming things, and off-by-one errors.
That's the extended version of the quote 😁
I had to get back all the way back in here, an year after you've uploaded.
Every single videos are pure gold.
Thanks again milan 👍👍
My pleasure! Glad this was helpful :)
In our project we implemented caching and invalidation as attributes on controller endpoints. Now we can add caching/invalidating in declarative way (It uses controller name and endpoint parameters as key value, when invalidate it removes everything related to specified controller). I'm wondering why other don't do the same because it's handy use and you cannot make a mistake when use caching in declarative way.
It would be better if you can control which parameters can form the cache key, but it's a great idea
I think Caching is an infrastructure concern, not an application concern. So, having the handlers invalidate the cache seems wrong. Also, having handlers that respond to domain/internal events seems wrong too.
I would consider the cache a form of persistence. So, I think the persistence infra code should deal with caching data.
Now if we are talking about output caching, that is a presentation layer concern, and it should probably be the presentation layer that deals with invalidating the output cache.
But, sometimes being pragmatic is better than being dogmatic.
What do you mean by "having handlers that respond to domain/internal events seems wrong too"? What's going to handle the events then?
Most of this can just as easily be moved to the Infrastructure layer.
@@MilanJovanovicTech probably just typed too fast. I mean having handlers for domain events integrating with caching.
Great video. Thank you Milan.
Glad it was helpful!
Hey, Milan!
Thanks for the caching related vidoes, great stuff. However, how would u go with invalidation if there were connections between resources?
Example: one Order -> many OrderItems.
You would need to invalidate 1) OrderItem by id 2) OrderItems filtering 3) Order that comes with his items.
I suppose, you would you still go with the events approach?
One event, and clear keys by pattern matching. Requires storing all keys however.
Hello Milan. What happens when you have more processes and some of them are changing DB and some of them are reading from cash at the same time from more API instances? I believe that concurrency issues may happen there. What to do to prevent that and have valid results all the time?
How about we keep cache invalidation operation atomic. In redis we can use Lua scripts/watch commands while invalidating cache - during this process other redis resources would freeze. However, our app instances which are reading (not updating) cache, will face delayed response during the process.
Bad design smells.
There will always be the case for discrepancy between the source of truth and the cache. That's one thing you have to be okay with if you want to use caching.
Great video 🎉
Glad you liked it!
I think caching lends itself better to the infrastructure layer, behing repository walls rather than just outside because a repository has explicit knowledge of when an entity is updated. Just a hunch. Havnt needed caching on that level so far. :)
This is generally how I do it as well. Making use of decorator pattern over a repository to make it a cached repository, any calls to update/delete/create will invalidate cache.
@@PelFox Can you post an example snippet of this method's implementation?
I'm not using a repository here, I'm doomed then 🤷♂️
How would you handle caching and invalidating paginated data in a single and multi-tenant scenarios? How do you decide to choose application caching over output response caching?
Just cache the appropriate cache key "page-1-page-size-15" etc and reset that
but if we want to use redis as a layer between API and Database , what we should do?
i mean persist data to disk and use redis as a write and read database(Cache) that after write data into redis, the data sent to an sql database.
I like the cache aside pattern
At 12:05, how do you invalidate a specific object from the cache without it's ID being used ? From what i read, the whole "products" caching is wiped in here, no id passed ?
Maybe i haven't understood something ?
Thanks !
Let's say the "products" key contains a list of all products or something similar .That was the idea
Nice content! Do we really need 3 Handlers for Cache Invalidation here? Can't we just publish the event with the cache key and use that key in the Handler to remove it from Cache?
Brilliant idea to be honest. My idea was more around generic events that can also be handled to do something else.
In your expensely project. You were caching the results of query which are implementing the ICacheableQuery. So what if I create a interface named ICacheInvalidatorCommand and write a function that returns the cache key/keys and invalidate those keys inside of a behaviour if the response is successful.
That was a brave attempt... I didn't want to complicate it for this video
Would love to see a more in depth cache invalidation video that covers different cache invalidation strategies, because this seems a bit simplistic and flawed. If products are changed often, then the caching would be constantly invalidated and might be considered pointless. I wonder if there's a better approach?
A more specific cache key and creating some "buckets" for more granular invalidation
@MilanJovanovicTech i imagine time based would also be acceptable if the business was okay with stale data for a certain period in order to have better performance. Alrho that probably only addresses the add/update scenario...since delete scenario could cause system errors if they try to open a deleted item from a cached list somewhere.
*patiently waiting for naming things video*
Should I even make one about that? 🤔
@@MilanJovanovicTech i dont think that one will get much attention but if you have spare time definitely haha maybe 5 minutes about tricks of naming, importance, general patterns, etc
Is it a good idea to override SaveChanges method of DbContext, and invalidate cache there?(key can be typeof(Entity).Name etc.) Or is your solution more reliable? What do you think?
I use this approach (overridding SaveChangensAsync or other UoW) and it works perfect for me. One place to deal with cache invalidation and forget about it. In DbContext there is access to ChangeTracker, so clearing "product-{id}" is no problem... I wondering why Milan didn't mension about that - is it something wrong with this solution, Milan? 🤔
It'll work, but gets overly complex as soon as your cache key is different from typeof(Entity).Name
One unrelated thing I've been wandering about is the cancellation token. When having steps like save saving to database and then pushing an event, wouldn't cancellation possibly lead to inconsistency? Specifically in case the request is cancelled after saving to database and before pushing the event.
Yes it could. I generally avoid using CancellationTokens in any Commands which does state changes.
Completely valid point! I'll do better in future videos.
What is the advantage of making libraries instead of folders in a project such as for models, services, repository etc....
I didn't really understand the question
I love every example that you do. BUT! you don't share the code that you have written, is there a reason why?
I share it on Patreon
Very nice and efficient way Milan! Thanks for sharing😊 Recently I applied a solution for this specific problem. My solution was the approach of Pipeline Behavior which MediatR gives us out of the box. Something like `CacheInvalidationBehavior` for specific commands (Create, Update, Delete) was enough for me. Do you think which one is more usable in this case? Pipeline Behavior or Notification Handler approach 🤔
How do you tell which commands need to invalidate the cache, and how do you know which cache keys?
@@MilanJovanovicTech Well, You can mark your commands with an interface, like `ICacheInvalidationRequest` and inside your behavior, you can check if the commands which hit the behavior implements that interface or not. If it's includes, you can invalidate your cache.
About the cache key, you can put a method inside of `ICacheInvalidationRequest` like `string GetCacheKey()` and the commands which implements this interface must implement this method also. So you can set cache key in there and use it in your `CacheInvalidationBehavior` class.
I guess it's harder to say than to do 😅
@@hamitkarahan2973 this is interesting do you have an example?
One question
1. Is there any specific reason of choosing record type for ProductDeleteEvent instead of class ?
I like using positional records because they are immutable
What about using interceptor and invalidate when products change
SaveChangesInterceptor? It'll grow pretty quickly when you add more entities into the mix
You didn’t really use the IPipelineBehavior for this. Instead you used Inotification which I guess also works. But do you have an example for using the pipeline behavior or this was it?
I don't have a pipeline behavior example. You could do something with an ICacheableComand interface + some way to tell the pipeline behavior which cache key to remove.
I don't think, it is a good idea to pass the CancellationToken to the methods after SaveChanges(cancellationToken).
Since you're not using a Outbox pattern, all methods that are executed after SaveChanges can be canceled and will also lead into inconsistent state, aren't they?
Noted, completely valid point!
What is the theme your are using?
ReSharper
Alright so far... now have one dbserver but multiple API server.. how do you invalidate multiple local caches effectively?
In that case you need a distributed cache, like Redis
I've recently fix a bug of cache invalidation. the key was dinamically generated using the name of the method and the value of the parameters and in one method to add an item the invalidation of the cache was called with the wrong list of parameters... Do you have naming convention for the cache key ? and also in case mediator pattern is not used what could be a way to centralize the invalidationof the key ?
To centralize invalidation, place it in some service/class. For naming convention, I like to be as descriptive as possible and use kebab-case
Hi ,
In my project I am caching Labs data (Not change very frequent) with related relationship properties ,do you think its a good idea to cache related info also ?
because everytime any lab changes its relationship property like (Working Days , Documents etc.) I need to reset the cache .
Mostly these related property will have count less than 10.
Example : caching labs with related properties.
public async Task GetAll(bool trackChanges = false)
{
return await FindAll(trackChanges)
.Include(x => x.WorkingDays)
.Include(x => x.Documents)
.Include(x => x.MediaList)
.Include(x => x.ServiceablePincodes)
.Include(x => x.Certificates)
.ToListAsync();
}
It's fine, you seem to know what you're doing 😁
Or you can just simply update the cache instead of invalidating it. It means that you only read from the cache that is up to date always.
An alternative
great
Thanks!
Our hardest problem to solve is cache of an object, and then someone else requests a list of the same object type. The cache is in redis and the data is in EF Core. On the surface it seems simple to look in the cache first and then get the rest.
But the problem is row level (i.e. object level) security which then has to merge results but do so by applying security criteria.
RavenDb largely solves this for us because it works like Redis itself when given enough memory, and thus our row level filtering queries intelligently use the cache whenever possible.
RavenDB is incredibly good, and so unerrated
@@MilanJovanovicTech absolutely but for those not using it to cheat the issue, providing guidance on how to handling the segmented nature and ability to know if you got all of the rows relevant from cache or if you have to go back to the database is a big complex deal that almost every developer at scale ends up dealing with. (And security at row level hits us all and has to be done intelligently with caching involved. )
Thanks. but your second solution has these cons as well 3:38
The point is this. Would you rather:
- Have your cache invalidation code scattered across many files
- Have it all in one place
What's your choice?
Sta ti studiras?
@@MilanJovanovicTech Good point. its a better approach.
but can we do something about this? I mean, the fact that we wrote that code in multiple places.
@@MilanJovanovicTech Domain events?
Nice, but I'd go for a simpler solution where I didn't have to maintain 13-14 classes just to implement a simple CRUD for one product ;-)
What would your solution look like?
I’ll come back when I have more experience
You can figure it out :)
If you can afford it - no cache is the best option 😂
And in most cases you can actually afford to
I agree that you shouldn't prematurely add caching, your code should work well enough without it. Often it's introduced early and developers think it's a must have. Your 10 users won't blow up the database, I can assure you.
@@PelFox but once more users use the application you will need it even if it’s just spikes
@@wolfVFXmc hopefully you are allowed to monitor your system. Very rarely it goes from 10 users to several thousands over a day, marketing takes time.
Why not just call the class "ProductCashInvalidator"🤔
Naming is the hard part
@@MilanJovanovicTech True my friend
I expected you'll speak about the real cache (wich is a really interesting topic) : cache, in computer science, is hardware L1, L2, L3 cache... so, be precise ! "Database cache" is what you are speaking about.
But this is an in-memory cache. And this is a software channel, not a hardware one.
@@MilanJovanovicTech No. You cannot say : cache implicitly mean "software data cache" : it is simply false, for me.
The fact is that your softwares are ALL TIME using hardware cache - without harware memory cache, your database cache may be 2-3 level of magnitude slower.
In a point of view of an experienced developper that start programming in 1990 and have made video games, there is no question about the signification of "cache" in computer science : at any time I'm doing software, I'm massivelly using hardware memory cache. So, if you say "I will discuss about cache" - it does not mean "database cache". "Database cache" is a high level strategy rellying on hardware caching systems...