dada
Jan 25, 2019
  16443
(9 votes)

Common Find caching pitfalls

Under certain conditions, the cache duration set with .StaticallyCacheFor(minutes) does not last as long as specified.
There are three common reasons for this.

More on .StatisticallyCacheFor can be found here
https://world.episerver.com/documentation/developer-guides/find/NET-Client-API/searching/Caching/


.Filter() on a datetime property

Any .Filter() on a datetime property with second (or finer) resolution should be rounded up to the nearest minutes matching the wanted cache duration. As the cache key is based on the query payload we don't want it to change more frequently than the cache duration otherwise we will rarely utilise the cache as a new cache is generated for every request and a new request sent to the Find service. 

Here is an example of such a filter extracted from the JSON sent to the _search endpoint. You could inspect your requests with Fiddler.

"range": {
"EndDate$$date": {
"from": "2019-01-24T20:59:59Z",
"to": "9999-12-31T23:59:59.9999999Z",
"include_lower": false,
"include_upper": true
}
}


.FilterForVisitor()

.FilterForVisitor is a set of filters including .ExcludeDelete, .PublishedInCurrentLanguage, and .FilterOnReadAccess.

.PublishedInCurrentLanguage contains a filter on a datetime and as such is affected by the same issue described in point #1.

In a later version of Find (13.1) the datetime sent through to the .PublishedInCurrentLanguage() method uses a NOW() function instead of an actual value and is therefor not affected by this.

If on an earlier version, replace .FilterForVisitor() with the individual filters and round up the datetime used with PublishedInCurrentLanguage() just as you would have if you've used a custom datetime filter as pointed out earlier.

.GetContentResult()

When using .GetContentResult() unlike .GetResult() there is a cache dependency on published content. If there are any updates cache will be evicted. For Commerce sites updates are typically frequent and could therefore have a negative impact on cache.

Our recommendation is to use a custom .GetContentResult() where cache with this dependency is bypassed. This is commented out here.
Don't forget to add your .StaticallyCacheFor() to control the cache duration.


public static IContentResult<TContentData> GetContentResult<TContentData>(this ITypeSearch<TContentData> search, int cacheForSeconds = 60, bool cacheForEditorsAndAdmins = false)
where TContentData : IContentData
{
if (typeof(IContent).IsAssignableFrom(typeof(IContent)))
{
search = search.Filter(x => ((IContentData)x).MatchTypeHierarchy(typeof(IContent)));
}
var projectedSearch = search
.Select(x =>
new ContentInLanguageReference(
new ContentReference(((IContent)x).ContentLink.ID, ((IContent)x).ContentLink.ProviderName),
((ILocalizable) x).Language.Name));

//Apply caching for non-editors
//if (cacheForEditorsAndAdmins || !(PrincipalInfo.HasEditAccess || PrincipalInfo.HasAdminAccess))
//{
// var cacheSettings = ServiceLocator.Current.GetInstance<IContentCacheKeyCreator>();
// projectedSearch = projectedSearch.StaticallyCacheFor(TimeSpan.FromSeconds(cacheForSeconds), new CacheDependency(null, new string[] { cacheSettings.RootKeyName }));
//}

var searchResult = projectedSearch.GetResult();

var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var fetchedContent = new Dictionary<ContentInLanguageReference, IContent>();
foreach (var language in searchResult.GroupBy(x => x.Language))
{
foreach (var content in contentRepository.GetItems(language.Select(x => x.ContentLink).ToList(), new LanguageSelector(language.Key)))
{
fetchedContent[new ContentInLanguageReference(content)] = content;
}
}

var sortedContent = new List<TContentData>();
foreach (var reference in searchResult)
{
IContent content;
if (fetchedContent.TryGetValue(reference, out content) && content is TContentData)
{
sortedContent.Add((TContentData)content);
}
else
{
log.WarnFormat("Search results contain reference to a content with reference \"{0}\" in language \"{1}\" but no such content could been found.", reference.ContentLink, reference.Language);
}
}

return new ContentResult<TContentData>(sortedContent, searchResult);
Jan 25, 2019

Comments

Binh Nguyen Thi
Binh Nguyen Thi Jan 28, 2019 07:34 AM

Hi dada,

Thanks for useful information. About .FilterForVisistor(), I am using EPiServer.Find 12.7.1 and see that the fix has been already available within NormalizeInMinutes method. So I am not sure that the exact version of EPiServer.Find that fixed this bug. 

Sven-Erik Jonsson
Sven-Erik Jonsson Jan 30, 2019 10:13 AM

If you want to get around the time based caching limitations brought on by '.FilterForVisitor()' in earlier versions of Find than 13.1 you can just implement your own IClient, ICommands, then implement your own SearchCommand and MultiSearchCommand that generates cache keys that are temporally indifferent. Piece of cake.

Andreas J
Andreas J Dec 17, 2019 04:01 PM

"When using .GetContentResult() unlike .GetResult() there is a cache dependency on published content. If there are any updates cache will be evicted. For Commerce sites updates are typically frequent and could therefore have a negative impact on cache."

Is this really true? I can see that GetContentResult() uses a cache dependency on IContentCacheKeyCreator.RootKeyName. It would be crazy if every every cached item with that dependency would be invalidated as soon as some (unrelated) commerce content changed.

I assume that many items are cached with root master keys, but I hope/think that those keys are only invalidated in "extreme" cases.

dada
dada May 15, 2020 09:31 AM

Hi Andreas,

Good catch. You are correct. When I wrote this blog post initially Find used another less optimal cache dependency (DataFactoryCache.VersionKey)

This was fixed in https://world.episerver.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=FIND-3039

Please login to comment.
Latest blogs
Upgrade RSS Feed Integration to Optimizely CMS 13 – v3.0.0 Beta

I’ve upgraded my  RSS Feed Integration library for Optimizely CMS to support Optimizely CMS 13. Version 3.0.0 is currently released as a beta to...

David Drouin-Prince | Feb 21, 2026 |

Multi Site NuGet v2 for Optimizely CMS 13 – Breaking Changes & Migration

The beta version 2 of DavidHome.Optimizely.MultiSite is now available on NuGet: https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite Thi...

David Drouin-Prince | Feb 21, 2026 |

Automate Your OCP Opal Tool Development with PowerShell

Creating an OCP (Optimizely Connect Platform) Opal Tool app from scratch can be time consuming and error prone. You need to set up the project...

Sanjay Kumar | Feb 21, 2026

Using HeadlessKit to build a head for an Optimizely SaaS CMS in .NET 10

Headless has a tendency to promise freedom and deliver alignment meetings. Two codebases. Two sets of models. Two teams trying very hard not to dri...

Allan Thraen | Feb 19, 2026 |

CMS 13 Preview 3: Key changes

If you've been following along with the CMS 13 preview, you've likely worked through the initial upgrade path covered in my previous post. Preview ...

Robert Svallin | Feb 19, 2026

A Tailwind CSS strategy for Visual Builder grids

More findings from using an Optimizely SaaS CMS instance; setting up a CSS strategy for rendering Visual Builder grids.

Johan Kronberg | Feb 18, 2026 |