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:
Kenn Jacobsen
2023-06-21 08:32:57 +02:00
committed by GitHub
parent dcd400810c
commit 0cdea6120b
7 changed files with 114 additions and 5 deletions

View File

@@ -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,

View 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);
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;
}
}