Invalidate Cache After Publishing

In order to improve performance and enhace the page loading time while using Search and Navigation, Optimizely provides an extension to cache the search results for a specific period of time. StaticallyCacheFor lets you store your query results, using a timestamp, in a cache with an auto-generated key. Once the defined timestamp has reached, the cache will be invalidated and should be populated by a new execution of the query.

You can also cache results with a dependency by adding a custom cache key applying an overload to the StaticallyCacheFor method.

.StaticallyCacheFor(TimeSpan.FromMinutes(5), new CacheDependency("CustomKey"))

Sometimes, you will need to force the invalidation of the cache when a specific content type is published. For example, lets say we have a menu where some items are beign generated based on page tags and we need to keep that list updated based on the uses of the tag (if any page is not using a tag, that tag won’t be listed).

[Categories]
[AllowedTypes(typeof(ResourceCategoryFacet))]
[Display(Name = "Resource Category", Description = "Category of the resource.",
	GroupName = CmsTabNames.Facets, Order = 40)]
public virtual IList<ContentReference> ResourceCategory { get; set; }

We must have a search service that will return a list, sorted alphabetically, of resource categories that are beign used at least once. But first, we need to create an extended property that will process the tag items and return a list of strings:

public static IEnumerable<string> ResourceCategoryFacet(this ResourcePageData page)
{
	if (page.ResourceCategory == null || !page.ResourceCategory.Any())
	{
		return null;
	}

	var items = page.ResourceCategory.GetItems<ResourceCategoryFacet>();
	return items.Select(x => x.Title);
}

And don’t forget to add the convention definition

SearchClient.Instance.Conventions.ForInstancesOf<ResourcePageData>().IncludeField(x => x.ResourceCategoryFacet());

Usually, when you extend your content type with this kind of properties, yo need to rebuild the index so these new preperties get indexed.

Then, we need to create the method on the search service that will retrieve all those tags and cached them to improve performance. This method will return a list of strings.

public IEnumerable<string> GetResourceCategories(int pageSize = 20)
{
	var cacheDependency = new CacheDependency(null, new[] { Global.CacheKeys.ResourceCategoryKey });

	var query = _findClient.Search<ResourcePageData>();
	query = query.FilterForVisitor()
		.TermsFacetFor(x => x.ResourceCategoryFacet(), r => r.Size = pageSize)
		.Take(0);
	var results = query.StaticallyCacheFor(TimeSpan.FromMinutes(60), cacheDependency).GetContentResult();

	var categoryFacets = results.TermsFacetFor(x => x.ResourceCategoryFacet());
	return categoryFacets.Terms.Select(x => x.Term).OrderBy(x => x);
}

Here you will see that there is a custom key that we have added to the StaticallyCacheFor method and defines a dependency.

public const string ResourceCategoryKey = "ResourceCategoryKey";

Finally, we need to create an event that will be triggered only when a specific type has been published (because not al pages have this tag field) so we don’t want to regenerate the search results everytime something is beign published. This event will invalidate the cache using the custom key and will force the cache to ble cleared so it can be populated with the latest data.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class PageEvents : IInitializableModule
{
	private readonly Lazy<CacheService> _cacheService = new Lazy<CacheService>(() => ServiceLocator.Current.GetInstance<CacheService>());
	private IContentEvents _contentEvents;

	public void Initialize(InitializationEngine context)
	{
		_contentEvents ??= ServiceLocator.Current.GetInstance<IContentEvents>();

		_contentEvents.PublishedContent += Instance_ContentChanged;
	}

	void Instance_ContentChanged(object sender, ContentEventArgs e)
	{
		if (sender == null || e == null) return;

		switch (e.Content)
		{
			case ResourcePageData _:
				_cacheService.Value.OnContentChange(Global.CacheKeys.ResourceCategoryKey);
				break;
			case CoursePageData _:
				_cacheService.Value.OnContentChange(Global.CacheKeys.CourseCategoryKey);
				break;
		}
	}

	public void Uninitialize(InitializationEngine context)
	{
		_contentEvents ??= ServiceLocator.Current.GetInstance<IContentEvents>();
		
		_contentEvents.PublishedContent -= Instance_ContentChanged;

	}
}

The CacheService contains a method that is beign called to invalidate cache by a custom key:

private readonly ISynchronizedObjectInstanceCache _cache;
public CacheService(ISynchronizedObjectInstanceCache cache)
{
	_cache = cache;
}
....
public void OnContentChange(string cacheKey)
{
	_cache.RemoveLocal(cacheKey);
	_cache.RemoveRemote(cacheKey);
}

And now, everytime you publish a specific content type, the cache of a navigation menu will be invalidated and will be populated with the latest changes. That’s how you can force to clear a cache when using the StaticallyCacheFor method. Hope this helps!

Here you can read more about caching and the use of StaticallyCacheFor.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: