using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PublishedCache; internal class PublishedElementPropertyBase : PublishedPropertyBase { protected readonly IPublishedElement Element; // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching // means faster execution, but uses memory - not sure if we want it // so making it configurable. private const bool FullCacheWhenPreviewing = true; private readonly Lock _locko = new(); private readonly object? _sourceValue; protected readonly bool IsMember; protected readonly bool IsPreviewing; private readonly ICacheManager? _cacheManager; private CacheValues? _cacheValues; private bool _interInitialized; private object? _interValue; private string? _valuesCacheKey; public PublishedElementPropertyBase( IPublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, ICacheManager? cacheManager, object? sourceValue = null) : base(propertyType, referenceCacheLevel) { _sourceValue = sourceValue; Element = element; IsPreviewing = previewing; _cacheManager = cacheManager; IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member; } // used to cache the CacheValues of this property // ReSharper disable InconsistentlySynchronizedField internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(Element.Key, Alias, IsPreviewing); public static string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) => "PublishedSnapshot.Property.CacheValues[" + (previewing ? "D:" : "P:") + contentUid + ":" + typeAlias + "]"; // ReSharper restore InconsistentlySynchronizedField public override bool HasValue(string? culture = null, string? segment = null) { var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); if (hasValue.HasValue) { return hasValue.Value; } GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); lock (_locko) { var value = GetInterValue(); hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); if (hasValue.HasValue) { return hasValue.Value; } CacheValues cacheValues = GetCacheValues(cacheLevel); if (!cacheValues.ObjectInitialized) { cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); cacheValues.ObjectInitialized = true; } value = cacheValues.ObjectValue; return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; } } public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue; private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) => GetCacheLevels(PropertyType.CacheLevel, out cacheLevel, out referenceCacheLevel); private void GetDeliveryApiCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) => GetCacheLevels(PropertyType.DeliveryApiCacheLevel, out cacheLevel, out referenceCacheLevel); private void GetDeliveryApiCacheLevelsForExpansion(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) => GetCacheLevels(PropertyType.DeliveryApiCacheLevelForExpansion, out cacheLevel, out referenceCacheLevel); private void GetCacheLevels(PropertyCacheLevel propertyTypeCacheLevel, out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) { // based upon the current reference cache level (ReferenceCacheLevel) and this property // cache level (PropertyType.CacheLevel), determines both the actual cache level for the // property, and the new reference cache level. // if the property cache level is 'shorter-termed' that the reference // then use it and it becomes the new reference, else use Content and // don't change the reference. // // examples: // currently (reference) caching at published snapshot, property specifies // elements, ok to use element. OTOH, currently caching at elements, // property specifies snapshot, need to use snapshot. if (propertyTypeCacheLevel > ReferenceCacheLevel || propertyTypeCacheLevel == PropertyCacheLevel.None) { cacheLevel = propertyTypeCacheLevel; referenceCacheLevel = cacheLevel; } else { cacheLevel = PropertyCacheLevel.Element; referenceCacheLevel = ReferenceCacheLevel; } } private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) { CacheValues cacheValues; switch (cacheLevel) { case PropertyCacheLevel.None: case PropertyCacheLevel.Snapshot: // never cache anything cacheValues = new CacheValues(); break; case PropertyCacheLevel.Element: // cache within the property object itself, ie within the content object cacheValues = _cacheValues ??= new CacheValues(); break; case PropertyCacheLevel.Elements: cacheValues = (CacheValues?)_cacheManager?.ElementsCache.Get(ValuesCacheKey, () => new CacheValues()) ?? new CacheValues(); break; default: throw new InvalidOperationException("Invalid cache level."); } return cacheValues; } private object? GetInterValue() { if (_interInitialized) { return _interValue; } _interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing); _interInitialized = true; return _interValue; } public override object? GetValue(string? culture = null, string? segment = null) { GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel); lock (_locko) { CacheValues cacheValues = GetCacheValues(cacheLevel); if (cacheValues.ObjectInitialized) { return cacheValues.ObjectValue; } cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); cacheValues.ObjectInitialized = true; return cacheValues.ObjectValue; } } public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null) { PropertyCacheLevel cacheLevel, referenceCacheLevel; if (expanding) { GetDeliveryApiCacheLevelsForExpansion(out cacheLevel, out referenceCacheLevel); } else { GetDeliveryApiCacheLevels(out cacheLevel, out referenceCacheLevel); } lock (_locko) { CacheValues cacheValues = GetCacheValues(cacheLevel); object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing, expanding); return expanding ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); } } private object? GetDeliveryApiDefaultObject(CacheValues cacheValues, Func getValue) { if (cacheValues.DeliveryApiDefaultObjectInitialized == false) { cacheValues.DeliveryApiDefaultObjectValue = getValue(); cacheValues.DeliveryApiDefaultObjectInitialized = true; } return cacheValues.DeliveryApiDefaultObjectValue; } private object? GetDeliveryApiExpandedObject(CacheValues cacheValues, Func getValue) { if (cacheValues.DeliveryApiExpandedObjectInitialized == false) { cacheValues.DeliveryApiExpandedObjectValue = getValue(); cacheValues.DeliveryApiExpandedObjectInitialized = true; } return cacheValues.DeliveryApiExpandedObjectValue; } protected class CacheValues { public bool ObjectInitialized; public object? ObjectValue; public bool XPathInitialized; public object? XPathValue; public bool DeliveryApiDefaultObjectInitialized; public object? DeliveryApiDefaultObjectValue; public bool DeliveryApiExpandedObjectInitialized; public object? DeliveryApiExpandedObjectValue; } }