diff --git a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs index bdb451e74e..5bac7f38f6 100644 --- a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs +++ b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategy.cs @@ -8,13 +8,15 @@ namespace Umbraco.Cms.Api.Delivery.Rendering; internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionStrategy { + private readonly IApiPropertyRenderer _propertyRenderer; private readonly bool _expandAll; private readonly string[] _expandAliases; private ExpansionState _state; - public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor) + public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor, IApiPropertyRenderer propertyRenderer) { + _propertyRenderer = propertyRenderer; (bool ExpandAll, string[] ExpanedAliases) initialState = InitialRequestState(httpContextAccessor); _expandAll = initialState.ExpandAll; _expandAliases = initialState.ExpanedAliases; @@ -24,7 +26,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt public IDictionary MapElementProperties(IPublishedElement element) => element.Properties.ToDictionary( p => p.Alias, - p => p.GetDeliveryApiValue(_state == ExpansionState.Expanding)); + p => GetPropertyValue(p, _state == ExpansionState.Expanding)); public IDictionary MapContentProperties(IPublishedContent content) => content.ItemType == PublishedItemType.Content @@ -66,7 +68,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt _state = ExpansionState.Expanding; } - var value = property.GetDeliveryApiValue(_state == ExpansionState.Expanding); + var value = GetPropertyValue(property, _state == ExpansionState.Expanding); // always revert to pending after rendering the property value _state = ExpansionState.Pending; @@ -84,7 +86,7 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt _state = ExpansionState.Expanded; var rendered = properties.ToDictionary( property => property.Alias, - property => property.GetDeliveryApiValue(false)); + property => GetPropertyValue(property, false)); _state = ExpansionState.Expanding; return rendered; } @@ -108,6 +110,9 @@ internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionSt : Array.Empty()); } + private object? GetPropertyValue(IPublishedProperty property, bool expanding) + => _propertyRenderer.GetPropertyValue(property, expanding); + private enum ExpansionState { Initial, diff --git a/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs b/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs new file mode 100644 index 0000000000..0ba90a6d91 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiPropertyRenderer +{ + object? GetPropertyValue(IPublishedProperty property, bool expanding); +} diff --git a/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs b/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs new file mode 100644 index 0000000000..82d92148bd --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public class ApiPropertyRenderer : IApiPropertyRenderer +{ + private readonly IPublishedValueFallback _publishedValueFallback; + + public ApiPropertyRenderer(IPublishedValueFallback publishedValueFallback) + => _publishedValueFallback = publishedValueFallback; + + public object? GetPropertyValue(IPublishedProperty property, bool expanding) + { + if (property.HasValue()) + { + return property.GetDeliveryApiValue(expanding); + } + + return _publishedValueFallback.TryGetValue(property, null, null, Fallback.To(Fallback.None), null, out var fallbackValue) + ? fallbackValue + : null; + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 49aceba913..62ce5d3aec 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -437,6 +437,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 3a8adb6a1e..886948697a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -40,6 +40,7 @@ public class DeliveryApiTests It.IsAny()) ).Returns("Default value"); deliveryApiPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + deliveryApiPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); deliveryApiPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); deliveryApiPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); @@ -54,6 +55,7 @@ public class DeliveryApiTests It.IsAny()) ).Returns("Default value"); defaultPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + defaultPropertyValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); defaultPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); DefaultPropertyType = SetupPublishedPropertyType(defaultPropertyValueConverter.Object, "default", "Default.Editor"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs index 1e12eef746..443dda29f4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs @@ -421,6 +421,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests var valueConverterMock = new Mock(); valueConverterMock.Setup(v => v.IsConverter(It.IsAny())).Returns(true); + valueConverterMock.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); valueConverterMock.Setup(v => v.ConvertIntermediateToDeliveryApiObject( @@ -457,7 +458,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests httpContextMock.SetupGet(c => c.Request).Returns(httpRequestMock.Object); httpContextAccessorMock.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); - IOutputExpansionStrategy outputExpansionStrategy = new RequestContextOutputExpansionStrategy(httpContextAccessorMock.Object); + IOutputExpansionStrategy outputExpansionStrategy = new RequestContextOutputExpansionStrategy(httpContextAccessorMock.Object, new ApiPropertyRenderer(new NoopPublishedValueFallback())); var outputExpansionStrategyAccessorMock = new Mock(); outputExpansionStrategyAccessorMock.Setup(s => s.TryGetValue(out outputExpansionStrategy)).Returns(true); @@ -584,6 +585,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests It.IsAny())) .Returns(() => apiElementBuilder.Build(element.Object)); elementValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); + elementValueConverter.Setup(p => p.IsValue(It.IsAny(), It.IsAny())).Returns(true); elementValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs new file mode 100644 index 0000000000..23aa82d146 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyRendererTests.cs @@ -0,0 +1,68 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; + +[TestFixture] +public class PropertyRendererTests : DeliveryApiTests +{ + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void NoFallback_YieldsPropertyValueWhenValueIsSet(object value) + { + var property = SetupProperty(value, true); + var renderer = new ApiPropertyRenderer(new NoopPublishedValueFallback()); + + Assert.AreEqual(value, renderer.GetPropertyValue(property, false)); + } + + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void NoFallback_YieldsNullWhenValueIsNotSet(object? value) + { + var property = SetupProperty(value, false); + var renderer = new ApiPropertyRenderer(new NoopPublishedValueFallback()); + + Assert.AreEqual(null, renderer.GetPropertyValue(property, false)); + } + + [TestCase(123)] + [TestCase("hello, world")] + [TestCase(null)] + [TestCase("")] + public void CustomFallback_YieldsCustomFallbackValueWhenValueIsNotSet(object? value) + { + var property = SetupProperty(value, false); + object? defaultValue = "Default value"; + var customPublishedValueFallback = new Mock(); + customPublishedValueFallback + .Setup(p => p.TryGetValue(property, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out defaultValue)) + .Returns(true); + var renderer = new ApiPropertyRenderer(customPublishedValueFallback.Object); + + Assert.AreEqual("Default value", renderer.GetPropertyValue(property, false)); + } + + private IPublishedProperty SetupProperty(object? value, bool isValue) + { + var propertyTypeMock = new Mock(); + propertyTypeMock.SetupGet(p => p.CacheLevel).Returns(PropertyCacheLevel.None); + propertyTypeMock.SetupGet(p => p.DeliveryApiCacheLevel).Returns(PropertyCacheLevel.None); + + var propertyMock = new Mock(); + propertyMock.Setup(p => p.PropertyType).Returns(propertyTypeMock.Object); + propertyMock.Setup(p => p.HasValue(It.IsAny(), It.IsAny())).Returns(isValue); + propertyMock + .Setup(p => p.GetDeliveryApiValue(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(value); + + return propertyMock.Object; + } +}