The power of Cache Dependencies in Sitefinity

Cache dependencies allow you to add dependencies to your cache expiration. For example, you could have a dependency on news. Then whenever news gets updated, your cache is automatically purged (and if you do it properly, it is purged on all environments if it is a load balanced setup).

Why use them?

They offer so much potential, but they are horribly underused in the Sitefinity developer community. So often, I see people developing widgets/pages without considering caching. In my consulting gig @ Progress, I regularly do performance audits and optimizations. It is incredible to me how much ignorance developers have in regards to coding for performance in Sitefinity. But that’s probably because many of the Sitefinity developers to date don’t have software engineering experience… But that discussion is for another time.

Anyways. Why use them? Because they are awesome. Combined with good coding and intelligent caching, they can help streamline the performance of your site by allowing you to cache nearly everything, and invalidate that cache only when necessary.

Page cache dependencies

The idea here is that you can invalidate the cache for a specific page whenever the content in its widgets are updated. Or whenever you want really. Sitefinity has some good examples of cache dependencies for Web Forms widgets here: https://docs.sitefinity.com/for-developers-implement-cache-dependencies. The code is pretty much the same for Feather too, its just added in different places.

These type of cache dependencies are subscribed directly to content types, like News. So whenever news is updated, the cache is purged. This works because Sitefinity already has hooks in their system for sending out cache dependency notifications whenever content is updated. Here are a couple of quick examples:

cacheDependencyNotifiedObjects.Add(new CacheDependencyKey() { Type = typeof(NewsItem) });

This code above is just a snippet of the code from the documentation article I linked earlier. It is subscribing to the NewsItem type. That means whenever any NewsItem is added/deleted/updated, it is going to purge the cache for whatever page has this widget on it.

result.Add(new CacheDependencyKey { Key = viewModel.Item.Fields.Id.ToString(), Type = contentResolvedType });

This code was taken from Sitefinity Feather (https://github.com/Sitefinity/feather/blob/44325f871528f384e3e9fa8fd21547487847fdd4/Telerik.Sitefinity.Frontend/Mvc/Models/ContentModelBase.cs). Pretty similar right? Except this one also has an Id in it. This is the cache subscription for the detail view of a content-based Feather widget. This allows us to invalidate this page’s cache only when that specific content item is updated/deleted.

If you are curious as to where Sitefinity is doing this work, it is on each individual content model. If you open one of the
Content-based models in JustDecompile (like NewsItem.cs) you will see the interface ICacheDependable. This is where it gathers the keys that will be notified when that content is updated. For the non-Content-based models (like Forum or Calendar), it uses the ICacheItem interface, which essentially just asks for the item’s Id.

Notification system

I mentioned before that Sitefinity has the notifications built in for all content types. What if you want to notify for a custom dependency? The notification system is available for your coding pleasure:

var cacheKeys = new List<CacheDependencyKey>();
// add the keys/titles that you are subscribing to
CacheDependency.Notify(cacheKeys);

Custom cache with dependencies

This is where things get cool. Page cache dependencies are the most common. But you can also implement caching and dependencies on anything. If you want to cache back a list of categories/tags in your site, simple as pie.

Alright, we are going to setup a pretty simple dynamic module for managing activities at Hogwarts School of Wizardry. Specifically, we will create a model called House and another model called Activity. Activities will be incidents where points were given or taken from a specific house. Whenever an activity is logged, we want to make sure we update the scoreboard. We will cache each House with their respective score and only recalculate the score when new activities are logged (using cache dependencies). I’ve pushed the project to Github here: https://github.com/avisra/SitefinityCacheSample

This sample doesn’t have a frontend. We only built a Web API Controller that outputs the data for each house. This is all we need to demonstrate the cache working. To request the API, you just go to the “/api/houses” route. This action uses a repository (HouseRepository.cs) to query/return the houses. If it doesn’t exist in cache already, it will also add it to cache with its dependencies (this all happens in HouseCache.cs).

Keep in mind that I will be using a dynamic module for this cache dependencies example. We don’t have control over the DynamicContent class so we will have to maneuver around its limitations. One of the main limitations is that we can’t modify its ICacheDependable implementation. Here is the implementation for DynamicContent:

    public IList<CacheDependencyKey> GetKeysOfDependentObjects()
    {
        var thisKey = new CacheDependencyKey() { Type = typeof(DynamicContent), Key = this.Id.ToString() };
        var allKey = new CacheDependencyKey() { Type = typeof(DynamicContent), Key = this.GetType().FullName };
        var statusKey = new CacheDependencyKey() { Type = typeof(DynamicContent), Key = string.Concat(this.Status.ToString(), this.GetType().FullName) };
        return new[] { thisKey, allKey, statusKey };
    }

Because of this, we can’t clear the cache of a House when its child items are updated. If we were working with a custom coded content type, we could just do the following for the ICacheDependable implementation of Activity.cs

    public IList<CacheDependencyKey> GetKeysOfDependentObjects()
    {
        var parentKey = new CacheDependencyKey() { Type = typeof(House), Key = this.Parent.Id.ToString() };
        var thisKey = new CacheDependencyKey() { Type = typeof(Activity), Key = this.Id.ToString() };
        return new[] { thisKey, parentKey };
    }

No worries though, because Sitefinity’s EventHub system fills the gap. With the EventHub we can subscribe to the events Sitefinity dispatches when dynamic content is created, updated, or deleted. We’ll use these to notify our cache when to purge. Here is our use of the events:

    public class Startup
    {
        public static void Application_Start()
        {
            Bootstrapper.Initialized += new EventHandler<ExecutedEventArgs>(Bootstrapper_Initialized);
        }

        private static void Bootstrapper_Initialized(object sender, ExecutedEventArgs e)
        {
            if (e.CommandName == "Bootstrapped")
            {
                if (Bootstrapper.IsDataInitialized)
                {
                    ObjectFactory.Container.RegisterType<ICacheManager, CacheManager>();

                    // Sitefinity has out of the box dependencies for each dynamic content item. But, it does notify the parent that one of the children have changed.
                    // So we are adding our own notifications so when a child item is added/updated/deleted, we notify the parent item so we can purge it from cache
                    // and recalculate the scoreboard.

                    // If this were a custom module/content type, we could completely control the notifications from the models!

                    EventHub.Subscribe<IDynamicContentCreatedEvent>(evt => IDynamicContentCreatedEvent(evt));
                    EventHub.Subscribe<IDynamicContentUpdatedEvent>(evt => IDynamicContentUpdatedEvent(evt));
                    EventHub.Subscribe<IDynamicContentDeletingEvent>(evt => IDynamicContentDeletingEvent(evt));
                }
            }
        }

        private static void IDynamicContentCreatedEvent(IDynamicContentCreatedEvent eventInfo)
        {
            if (eventInfo.Item.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live)
            {
                if (eventInfo.Item.GetType() == HogwartsConstants.activityType)
                {
                    // A new activity was created. Purge the cache for whichever house it belongs to
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = eventInfo.Item.SystemParentId.ToString(), Type = HogwartsConstants.houseType } });
                }
                else if (eventInfo.Item.GetType() == HogwartsConstants.houseType)
                {
                    // House has been created. Purge the HouseKeys cache so the new house appears in the list
                    // ... Yes. I know that Hogwarts will only ever have 4 houses...
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = HogwartsConstants.cacheKeysInstanceName, Type = HogwartsConstants.houseType } });
                }
            }
        }

        private static void IDynamicContentUpdatedEvent(IDynamicContentUpdatedEvent eventInfo)
        {
            if (eventInfo.Item.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live)
            {
                if (eventInfo.Item.GetType() == HogwartsConstants.activityType)
                {
                    // An activity was modified. Purge the cache for whichever house it belongs to
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = eventInfo.Item.SystemParentId.ToString(), Type = HogwartsConstants.houseType } });
                }
                else if (eventInfo.Item.GetType() == HogwartsConstants.houseType)
                {
                    // House has been edited. Purge it's cache (so new title or logo loads in)
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = eventInfo.Item.Id.ToString(), Type = HogwartsConstants.houseType } });
                }
            }
        }

        private static void IDynamicContentDeletingEvent(IDynamicContentDeletingEvent eventInfo)
        {
            // Notice we are using IDynamicContentDeletingEvent instead of IDynamicContentDeletedEvent
            // The only difference is that Deleting occurs before the transation is submitted to the database. But, it does occur after permission checks. The only reason this would fail
            // would be due to do a bug or database connection. We have to do this because IDynamicContentDeletedEvent occurs AFTER it has been deleted from the database. And Sitefinity
            // doesn't have access to the SystemParentId at that point.

            if (eventInfo.Item.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live)
            {
                if (eventInfo.Item.GetType() == HogwartsConstants.activityType)
                {
                    // An activity was deleted. Purge the cache for whichever house it belongs to
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = eventInfo.Item.SystemParentId.ToString(), Type = HogwartsConstants.houseType } });
                }
                else if (eventInfo.Item.GetType() == HogwartsConstants.houseType)
                {
                    // House has been deleted. Purge the HouseKeys cache so the house disappears from the list
                    // ... Yes. I know that Hogwarts will only ever have 4 houses...
                    CacheDependency.Notify(new List<CacheDependencyKey>() { new CacheDependencyKey() { Key = HogwartsConstants.cacheKeysInstanceName, Type = HogwartsConstants.houseType } });
                }
            }
        }
    }

For our repository, we have two public methods: GetHouses() and GetHouse(Guid id). These methods will only query from the database if the data doesn’t already exist in cache (and if it doesn’t exist in cache, it will add it). Because we don’t have a good way of retrieving the list of houses in cache, we added a separate object to cache for all of the house keys (HouseCache.AddKeys / HouseCache.GetKeys). It isn’t really applicable in this Hogwarts example, because there will always only ever be 4 houses… But it’s helpful in other projects because it allows you to only reload the list of houses if a house has been added or deleted. Here is our repository:

    public class HouseRepository
    {
        private readonly DynamicModuleManager manager;

        public HouseRepository(DynamicModuleManager manager)
        {
            this.manager = manager;
        }

        public IEnumerable<House> GetHouses()
        {
            using (new ElevatedModeRegion(this.manager))
            {
                var keys = HouseCache.GetKeys();

                // if cache is empty, initialize the cache
                if (keys == null)
                {
                    var houses = LoadHouses();
                    keys = houses.Select(m => m.Id.ToString()).ToList();
                    HouseCache.AddKeys(keys);
                }

                List<House> houseModels = new List<House>();
                foreach (var key in keys)
                {
                    houseModels.Add(GetHouse(Guid.Parse(key)));
                }

                return houseModels;
            }
        }

        public House GetHouse(Guid id)
        {
            using (new ElevatedModeRegion(this.manager))
            {
                var house = HouseCache.Get(id.ToString());
                if (house == null)
                {
                    var houses = this.LoadHouses().ToList();
                    house = houses.FirstOrDefault(h => h.Id == id);
                    if (house != null)
                    {
                        var activities = this.manager.GetChildItems(new List<Guid>() { house.Id }, HogwartsConstants.activityType).Where(a => a.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live).ToList();
                        house.Points = activities != null && activities.Count > 0 ? Convert.ToInt32(activities.Select(a => a.GetValue<decimal>("Points")).Sum()) : 0;
                        HouseCache.Add(house.Id.ToString(), house);
                    }
                    else
                    {
                        house = null;
                    }
                }
                return house;
            }
        }

        private IEnumerable<House> LoadHouses()
        {
            using (new ElevatedModeRegion(this.manager))
            {
                return this.manager.GetDataItems(HogwartsConstants.houseType).Where(h => h.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live).Select(h => new House(h));
            }
        }
    }

And finally, here is our cache:

    public static class HouseCache
    {
        public static void Add(string key, House value)
        {
            var cacheDependency = new CompoundCacheDependency();

            // Subscribe to whenever the system receives messages to purge cache for this house
            cacheDependency.CacheDependencies.Add(new DataItemCacheDependency(HogwartsConstants.houseType, value.Id));

            // You can add additional dependencies here if needed

            lock (housesSync)
            {
                ICacheManager housesCache = SystemManager.GetCacheManager(HogwartsConstants.cacheInstanceName);
                // Add house domain model to cache with all dependencies
                housesCache.Add(key, value, CacheItemPriority.Normal, null, new ICacheItemExpiration[] { cacheDependency });
            }
        }

        public static House Get(string key)
        {
            lock (housesSync)
            {
                ICacheManager housesCache = SystemManager.GetCacheManager(HogwartsConstants.cacheInstanceName);
                if (housesCache.Contains(key))
                    return housesCache.GetData(key) as House;
            }

            return null;
        }

        #region Collection

        public static void AddKeys(IEnumerable<string> houseIds)
        {
            lock (housesSync)
            {
                ICacheManager housesCache = SystemManager.GetCacheManager(HogwartsConstants.cacheInstanceName);
                housesCache.Add("house_ids", houseIds, CacheItemPriority.Normal, null, new DataItemCacheDependency(HogwartsConstants.houseType, HogwartsConstants.cacheKeysInstanceName));
            }
        }

        public static IEnumerable<string> GetKeys()
        {
            lock (housesSync)
            {
                ICacheManager housesCache = SystemManager.GetCacheManager(HogwartsConstants.cacheInstanceName);
                return housesCache.GetData("house_ids") as IEnumerable<string>;
            }
        }

        public static IEnumerable<House> All()
        {
            lock (housesSync)
            {
                ICacheManager housesCache = SystemManager.GetCacheManager(HogwartsConstants.cacheInstanceName);
                List<House> allHouses = new List<House>();
                var keys = housesCache.GetData("house_ids") as IEnumerable<string>;
                foreach (var key in keys)
                {
                    if (housesCache.Contains(key))
                        allHouses.Add(housesCache.GetData(key) as House);
                }
                return allHouses;
            }
        }

        #endregion

        private static readonly object housesSync = new object();
    }

In order for our cache class to work, we had to add it to config and register it at application startup. The config portion is in SystemConfig.config:

    <cacheManagers>
        <add name="Houses" />
    </cacheManagers>

And we register it in our startup code which was posted earlier (with all of the events):

    ObjectFactory.Container.RegisterType<ICacheManager, CacheManager>();

This setup is great for boosting performance. It is also inherently compatible with load balancing. Because cache is stored in-memory, each environment will have its own cache of houses. Each one will cache the houses on the first request. And whenever the system sends out notifications to purge cache (CacheDependency.Notify), it sends it to all environments in the setup. So each environment will purge its cache, even when the change technically went through on a single environment.

Let me know if you have any questions on this setup. Until then, happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *