Following a question posted at the Umbraco Web Developers group on Facebook asking how to use data from specific nodes in several places without having to load them all over each time, I promised to share the way I've been doing it for quite a long time now.
So you probably have something in the lines of the following screenshot in your Umbraco site, where you have to reuse some of those nodes again and again and again:
I had the same issue, so I started writing some code to make my life a bit easier and have some of my "special" nodes cached,
The general idea is the following:
- Have a "Cached Nodes Builder" along with its composer. This is the general-use component with which you can request a node of a specific type (Models Builder types), return it and cache it for future use. This works with nodes and sets of nodes. Although the functionality for sets of nodes is there, there's no implementation for sets of nodes on the example below for the time being, but it's fairly easy to use it if you need it.
- Have a "Cached Nodes" class that uses "Cached Nodes Builder" and implements static methods that return strongly-typed documents.
- Have an event handler attached to all document handling events (delete, publish, unpublish, etc.) that invalidate the cached documents whenever an action occurs. It is very important to invalidate the cache after every change or you run the danger of having an older IPublishedSnapshot in place (look at the remarks listed at the end of the post). Alternatively, you can go with an id-only caching approach and get a fresh document each time.
PROS: Obvious :)
CONS: You can have only one cached node per document type (that's the idea under which the whole concept works). Normally you'll only be caching "special" nodes that have their own doctype, but if you need something more than that then you may skip the "Cached Nodes Builder" call and implement your own logic in the respective method you'll implement inside the CachedNodes class. Oh, and you have to be using strongly-typed models (i.e. Models Builder) for this to work.
The code you'll see below is for Umbraco v8 (explained right afterwards):
So the CachedNodesBuilder class, as well as its composer, are our little "black boxes" - you request a node of a specific type, it traverses the document tree once, finds the first node that is the given type, caches it, and returns it, returning the cached version from that point on.
The SiteEventsCM class is a very general event handler which invalidates the runtime cache for the CachedNodesBuilder in any change to the document tree. This could be as precise as you need it - e.g. invalidate the cache only when documents of specific doctypes are changed.
Finally, the CachedNodes static class and its methods is the one you'll be actually using. I've used three doctypes as examples - PageHome, ConfigGlobalSettings, and ConfigScriptSettings. In your implementation, you should change those to your own doctypes. Or, you can make the private generic method below (GetNode<T>) public and call it directly if you like, without implementing other doctype-specific methods.
(I hope you don't need an explanation for the CacheLiteralsCM class :) )
Getting a cached node is simple. Suppose you have a doctype called "MySettings" and you have already implemented your "GetMySettings" method in the CachedNodes class as below:
public static MySettings GetMySettings(int pageId = 0) { return GetNode(pageId); }
So in any view, controller, or anything else for which you might need this node, you can just do:
var settings = CachedNodes.GetMySettings();
And just use the settings variable from that point on.
If you're doing AJAX calls, though, you MUST also pass the id of the current page (hence the optional pageId parameter), this is because there's no reference point of what the current page is when an AJAX call is made. In all other circumstances you don't have to pass anything to the CachedNodesBuilder.
Note: One size doesn't fit all - this specific implementation may not suit your own needs, but I'll be happy to know in case it does :) This code is provided only as a reference point - be sure to adapt it to your own needs.
UPDATE: After this post was published on the Umbraco Slack channel, several useful remarks were posted by other community members, either suggesting alternative approaches or pointing out weaknesses in this one. I think that if you've still reading this you may benefit from those in making a decision whether to follow the path I'm suggesting in the post or something else. So here goes:
The nodes are already in the Umbraco cache, so you're effectively caching cached things: True, but still. Querying the tree to find the node each time is the expensive factor here, and that's what I'm trying to avoid. I've seen a lot of attempts from people trying to limit querying for "special" nodes in the past, the most devastating one being hardcoding IDs. That's how the idea for a caching system came up. Having the nodes in the Umbraco cache is not the same thing IMHO. You still have to query to find them.
Querying things in V8 using Descendants is not as slow as in V7, so maybe that's a lot of effort for just a little performance improvement: I'm aware that there have been huge performance improvements on V8, but I still think no querying at all is better than the alternative.
Caching the IPublishedContent will give you more problems than it solves, especially if you consider that Umbraco ensures all content fetched within a single request is from the same IPublishedSnapshot and therefore consistent: True, that's why I'm invalidating cache in every change. I have been a victim of this myself! But since the cache is invalidated in EVERY change, I think this goes away since IPublishedSnapshot will always reflect the most current context.
Why cache the whole object and not just the key: This is also effective - and, at the point IPublishedSnapshot doesn't get in the way, it's just a matter of preference IMHO. It could be simpler if there was just the key though, admittedly. It's an excellent alternative to avoid IPublishedSnapshot issues.
It won't work in Load Balanced setups if you don't use ICacheRefresher so all frontend servers get notified, not only the master server: True. The example given in the post does not take LB setups into account.
Special" docs not usually being deep into the hierarhcy so caching is not really offering anything in terms of performance: A valid point. But, based on past experience, I've needed to cache stuff that was indeed buried deep into the hierarchy (> level 3-4). E.g.: A "careers" root page on the 3rd level, or a "partners" root page on a similar level. Those can indeed hurt performance if the hierarchy is queried for them all the time. The solution I'm proposing is level-independent.
Adding settings to the parent/root node instead of using "special" nodes is more effective: This is a whole different chapter which I'd be happy to discuss in depth. I've gone with that approach for a long time in the past, and realized it's good only when you have a few settings - but when you have a whole bunch of them it becomes tedious. Also, this caching approach is not only related to settings nodes.
Extend the UmbracoHelper class instead: This is indeed an interesting approach as an alternative. Time Geyssens has posted an excellent article on how to do this here: https://dev.to/timgeyssens/extending-the-umbracohelper-class-in-umbraco-v8-3l0l
Happy coding!