Performance: Request cache referenced entities when saving documents with block editors (#20590)

* Added request cache to content and media lookups in mult URL picker.

* Allow property editors to cache referenced entities from block data.

* Update src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add obsoletions.

* Minor spellcheck

* Ensure request cache is available before relying on it.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
Andy Butland
2025-10-31 10:49:26 +01:00
parent 417335a5c6
commit 95cc6cc67b
7 changed files with 368 additions and 41 deletions

View File

@@ -3,12 +3,14 @@ using System.Globalization;
using System.Runtime.Serialization;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -20,6 +22,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataContract]
public class DataValueEditor : IDataValueEditor
{
private const string ContentCacheKeyFormat = nameof(DataValueEditor) + "_Content_{0}";
private const string MediaCacheKeyFormat = nameof(DataValueEditor) + "_Media_{0}";
private readonly IJsonSerializer? _jsonSerializer;
private readonly IShortStringHelper _shortStringHelper;
@@ -415,4 +420,155 @@ public class DataValueEditor : IDataValueEditor
return value.TryConvertTo(valueType);
}
/// <summary>
/// Retrieves a <see cref="IContent"/> instance by its unique identifier, using the provided request cache to avoid redundant
/// lookups within the same request.
/// </summary>
/// <remarks>
/// This method caches content lookups for the duration of the current request to improve performance when the same content
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
/// </remarks>
/// <param name="key">The unique identifier of the content item to retrieve.</param>
/// <param name="requestCache">The request-scoped cache used to store and retrieve content items for the duration of the current request.</param>
/// <param name="contentService">The content service used to fetch the content item if it is not found in the cache.</param>
/// <returns>The <see cref="IContent"/> instance corresponding to the specified key, or null if no such content item exists.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static IContent? GetAndCacheContentById(Guid key, IRequestCache requestCache, IContentService contentService)
{
if (requestCache.IsAvailable is false)
{
return contentService.GetById(key);
}
var cacheKey = string.Format(ContentCacheKeyFormat, key);
IContent? content = requestCache.GetCacheItem<IContent?>(cacheKey);
if (content is null)
{
content = contentService.GetById(key);
if (content is not null)
{
requestCache.Set(cacheKey, content);
}
}
return content;
}
/// <summary>
/// Adds the specified <see cref="IContent"/> item to the request cache using its unique key.
/// </summary>
/// <param name="content">The content item to cache.</param>
/// <param name="requestCache">The request cache in which to store the content item.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static void CacheContentById(IContent content, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return;
}
var cacheKey = string.Format(ContentCacheKeyFormat, content.Key);
requestCache.Set(cacheKey, content);
}
/// <summary>
/// Retrieves a <see cref="IMedia"/> instance by its unique identifier, using the provided request cache to avoid redundant
/// lookups within the same request.
/// </summary>
/// <remarks>
/// This method caches media lookups for the duration of the current request to improve performance when the same media
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
/// </remarks>
/// <param name="key">The unique identifier of the media item to retrieve.</param>
/// <param name="requestCache">The request-scoped cache used to store and retrieve media items for the duration of the current request.</param>
/// <param name="mediaService">The media service used to fetch the media item if it is not found in the cache.</param>
/// <returns>The <see cref="IMedia"/> instance corresponding to the specified key, or null if no such media item exists.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static IMedia? GetAndCacheMediaById(Guid key, IRequestCache requestCache, IMediaService mediaService)
{
if (requestCache.IsAvailable is false)
{
return mediaService.GetById(key);
}
var cacheKey = string.Format(MediaCacheKeyFormat, key);
IMedia? media = requestCache.GetCacheItem<IMedia?>(cacheKey);
if (media is null)
{
media = mediaService.GetById(key);
if (media is not null)
{
requestCache.Set(cacheKey, media);
}
}
return media;
}
/// <summary>
/// Adds the specified <see cref="IMedia"/> item to the request cache using its unique key.
/// </summary>
/// <param name="media">The media item to cache.</param>
/// <param name="requestCache">The request cache in which to store the media item.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static void CacheMediaById(IMedia media, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return;
}
var cacheKey = string.Format(MediaCacheKeyFormat, media.Key);
requestCache.Set(cacheKey, media);
}
/// <summary>
/// Determines whether the content item identified by the specified key is present in the request cache.
/// </summary>
/// <param name="key">The unique identifier for the content item to check for in the cache.</param>
/// <param name="requestCache">The request cache in which to look for the content item.</param>
/// <returns>true if the content item is already cached in the request cache; otherwise, false.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static bool IsContentAlreadyCached(Guid key, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return false;
}
var cacheKey = string.Format(ContentCacheKeyFormat, key);
return requestCache.GetCacheItem<IContent?>(cacheKey) is not null;
}
/// <summary>
/// Determines whether the media item identified by the specified key is present in the request cache.
/// </summary>
/// <param name="key">The unique identifier for the media item to check for in the cache.</param>
/// <param name="requestCache">The request cache in which to look for the media item.</param>
/// <returns>true if the media item is already cached in the request cache; otherwise, false.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static bool IsMediaAlreadyCached(Guid key, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return false;
}
var cacheKey = string.Format(MediaCacheKeyFormat, key);
return requestCache.GetCacheItem<IMedia?>(cacheKey) is not null;
}
}

View File

@@ -0,0 +1,19 @@
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Optionally implemented by property editors, this defines a contract for caching entities that are referenced in block values.
/// </summary>
[Obsolete("This interface is available for support of request caching retrieved entities in property value editors that implement it. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
public interface ICacheReferencedEntities
{
/// <summary>
/// Caches the entities referenced by the provided block data values.
/// </summary>
/// <param name="values">An enumerable collection of block values that may contain the entities to be cached.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
void CacheReferencedEntities(IEnumerable<object> values);
}