Add support for property value fallbacks in the delivery API (#14421)
* Add support for property value fallbacks in the delivery API * Add dedicated tests for the IDeliveryApiPropertyValueConverter interface * Rewrite for less impact and more streamlined with Razor output
This commit is contained in:
@@ -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<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> element.Properties.ToDictionary(
|
||||
p => p.Alias,
|
||||
p => p.GetDeliveryApiValue(_state == ExpansionState.Expanding));
|
||||
p => GetPropertyValue(p, _state == ExpansionState.Expanding));
|
||||
|
||||
public IDictionary<string, object?> 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<string>());
|
||||
}
|
||||
|
||||
private object? GetPropertyValue(IPublishedProperty property, bool expanding)
|
||||
=> _propertyRenderer.GetPropertyValue(property, expanding);
|
||||
|
||||
private enum ExpansionState
|
||||
{
|
||||
Initial,
|
||||
|
||||
8
src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs
Normal file
8
src/Umbraco.Core/DeliveryApi/IApiPropertyRenderer.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiPropertyRenderer
|
||||
{
|
||||
object? GetPropertyValue(IPublishedProperty property, bool expanding);
|
||||
}
|
||||
23
src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs
Normal file
23
src/Umbraco.Core/DeliveryApi/PropertyRenderer.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -437,6 +437,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiPublishedContentCache, ApiPublishedContentCache>();
|
||||
builder.Services.AddSingleton<IApiRichTextElementParser, ApiRichTextElementParser>();
|
||||
builder.Services.AddSingleton<IApiRichTextMarkupParser, ApiRichTextMarkupParser>();
|
||||
builder.Services.AddSingleton<IApiPropertyRenderer, ApiPropertyRenderer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public class DeliveryApiTests
|
||||
It.IsAny<bool>())
|
||||
).Returns("Default value");
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
@@ -54,6 +55,7 @@ public class DeliveryApiTests
|
||||
It.IsAny<bool>())
|
||||
).Returns("Default value");
|
||||
defaultPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
defaultPropertyValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
defaultPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
DefaultPropertyType = SetupPublishedPropertyType(defaultPropertyValueConverter.Object, "default", "Default.Editor");
|
||||
|
||||
@@ -421,6 +421,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
|
||||
var valueConverterMock = new Mock<IDeliveryApiPropertyValueConverter>();
|
||||
valueConverterMock.Setup(v => v.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
valueConverterMock.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).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<IOutputExpansionStrategyAccessor>();
|
||||
outputExpansionStrategyAccessorMock.Setup(s => s.TryGetValue(out outputExpansionStrategy)).Returns(true);
|
||||
|
||||
@@ -584,6 +585,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
It.IsAny<bool>()))
|
||||
.Returns(() => apiElementBuilder.Build(element.Object));
|
||||
elementValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
|
||||
@@ -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<IPublishedValueFallback>();
|
||||
customPublishedValueFallback
|
||||
.Setup(p => p.TryGetValue(property, It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<Fallback>(), It.IsAny<object?>(), 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<IPublishedPropertyType>();
|
||||
propertyTypeMock.SetupGet(p => p.CacheLevel).Returns(PropertyCacheLevel.None);
|
||||
propertyTypeMock.SetupGet(p => p.DeliveryApiCacheLevel).Returns(PropertyCacheLevel.None);
|
||||
|
||||
var propertyMock = new Mock<IPublishedProperty>();
|
||||
propertyMock.Setup(p => p.PropertyType).Returns(propertyTypeMock.Object);
|
||||
propertyMock.Setup(p => p.HasValue(It.IsAny<string?>(), It.IsAny<string?>())).Returns(isValue);
|
||||
propertyMock
|
||||
.Setup(p => p.GetDeliveryApiValue(It.IsAny<bool>(), It.IsAny<string?>(), It.IsAny<string?>()))
|
||||
.Returns(value);
|
||||
|
||||
return propertyMock.Object;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user