@@ -148,8 +148,9 @@ await _hybridCache.SetAsync(
148148 // If we can resolve the content cache node, we still need to check if the ancestor path is published.
149149 // This does cost some performance, but it's necessary to ensure that the content is actually published.
150150 // When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
151- // Similarly, when a branch is published, next time the content is requested, the parent will be published,
152- // this works because we don't cache null values.
151+ // Similarly, when a branch is published, next time the content is requested, the parent will be published.
152+ // Null values are cached here are tagged and cleared by ClearMemoryCacheAsync, so the next request after a
153+ // cache clear will re-query the database.
153154 if ( preview is false && contentCacheNode is not null && _publishStatusQueryService . HasPublishedAncestorPath ( contentCacheNode . Key ) is false )
154155 {
155156 // Careful not to early return here. We need to complete the scope even if returning null.
@@ -333,10 +334,25 @@ public async Task RefreshContentAsync(IContent content)
333334
334335 private static string GetCacheKey ( Guid key , bool preview ) => preview ? $ "{ key } +draft" : $ "{ key } ";
335336
336- // Generates the cache tags for a given CacheNode
337- // We use the tags to be able to clear all cache entries that are related to a given content item.
338- // Tags for now are only content/media, but can be expanded with draft/published later.
339- private static HashSet < string > GenerateTags ( ContentCacheNode ? cacheNode ) => cacheNode is null ? [ ] : [ Constants . Cache . Tags . Content , ContentTypeIdTag ( cacheNode . ContentTypeId ) ] ;
337+ /// <summary>
338+ /// Generates the cache tags for a given <see cref="ContentCacheNode"/>.
339+ /// </summary>
340+ /// <param name="cacheNode">The cache node to generate tags for, or <c>null</c> for a negative-cache entry.</param>
341+ /// <returns>
342+ /// A set of tags that always includes <see cref="Constants.Cache.Tags.Content"/>.
343+ /// When <paramref name="cacheNode"/> is non-null, the content type ID tag is also included.
344+ /// </returns>
345+ /// <remarks>
346+ /// Tags are used to clear all cache entries related to a given content item or type.
347+ /// The <see cref="Constants.Cache.Tags.Content"/> tag is always included — even for null entries — so
348+ /// that <see cref="ClearMemoryCacheAsync"/> (which clears by this tag) can evict negative-cache entries.
349+ /// Without this, null entries survive tag-based cache clears and become permanently stale.
350+ /// Tags currently cover content/media distinctions but can be expanded with draft/published later.
351+ /// </remarks>
352+ private static HashSet < string > GenerateTags ( ContentCacheNode ? cacheNode ) =>
353+ cacheNode is null
354+ ? [ Constants . Cache . Tags . Content ]
355+ : [ Constants . Cache . Tags . Content , ContentTypeIdTag ( cacheNode . ContentTypeId ) ] ;
340356
341357 public async Task DeleteItemAsync ( IContentBase content )
342358 {
0 commit comments