diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
index 877e662da7..dab8ccd6d2 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByIdContentApiController : ContentApiItemControllerBase
{
private readonly IRequestMemberAccessService _requestMemberAccessService;
@@ -48,18 +49,31 @@ public class ByIdContentApiController : ContentApiItemControllerBase
: base(apiPublishedContentCache, apiContentResponseBuilder)
=> _requestMemberAccessService = requestMemberAccessService;
- ///
- /// Gets a content item by id.
- ///
- /// The unique identifier of the content item.
- /// The content item or not found result.
[HttpGet("item/{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
public async Task ById(Guid id)
+ => await HandleRequest(id);
+
+ ///
+ /// Gets a content item by id.
+ ///
+ /// The unique identifier of the content item.
+ /// The content item or not found result.
+ [HttpGet("item/{id:guid}")]
+ [MapToApiVersion("2.0")]
+ [ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ByIdV20(Guid id)
+ => await HandleRequest(id);
+
+ private async Task HandleRequest(Guid id)
{
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
index df7e3b26a4..a58b88a532 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
@@ -12,6 +12,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByIdsContentApiController : ContentApiItemControllerBase
{
private readonly IRequestMemberAccessService _requestMemberAccessService;
@@ -49,17 +50,29 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
: base(apiPublishedContentCache, apiContentResponseBuilder)
=> _requestMemberAccessService = requestMemberAccessService;
- ///
- /// Gets content items by ids.
- ///
- /// The unique identifiers of the content items to retrieve.
- /// The content items.
[HttpGet("item")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
public async Task Item([FromQuery(Name = "id")] HashSet ids)
+ => await HandleRequest(ids);
+
+ ///
+ /// Gets content items by ids.
+ ///
+ /// The unique identifiers of the content items to retrieve.
+ /// The content items.
+ [HttpGet("items")]
+ [MapToApiVersion("2.0")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task ItemsV20([FromQuery(Name = "id")] HashSet ids)
+ => await HandleRequest(ids);
+
+ private async Task HandleRequest(HashSet ids)
{
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
index 4806db45ff..b88147999a 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
@@ -13,6 +13,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByRouteContentApiController : ContentApiItemControllerBase
{
private readonly IRequestRoutingService _requestRoutingService;
@@ -73,6 +74,16 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
_requestMemberAccessService = requestMemberAccessService;
}
+ [HttpGet("item/{*path}")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task ByRoute(string path = "")
+ => await HandleRequest(path);
+
///
/// Gets a content item by route.
///
@@ -83,12 +94,15 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
///
/// The content item or not found result.
[HttpGet("item/{*path}")]
- [MapToApiVersion("1.0")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task ByRoute(string path = "")
+ public async Task ByRouteV20(string path = "")
+ => await HandleRequest(path);
+
+ private async Task HandleRequest(string path)
{
path = DecodePath(path);
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
index 2f4e8af9c8..d726027021 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
@@ -15,6 +15,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class QueryContentApiController : ContentApiControllerBase
{
private readonly IRequestMemberAccessService _requestMemberAccessService;
@@ -45,6 +46,20 @@ public class QueryContentApiController : ContentApiControllerBase
_requestMemberAccessService = requestMemberAccessService;
}
+ [HttpGet]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task Query(
+ string? fetch,
+ [FromQuery] string[] filter,
+ [FromQuery] string[] sort,
+ int skip = 0,
+ int take = 10)
+ => await HandleRequest(fetch, filter, sort, skip, take);
+
///
/// Gets a paginated list of content item(s) from query.
///
@@ -55,16 +70,19 @@ public class QueryContentApiController : ContentApiControllerBase
/// The amount of items to take.
/// The paged result of the content item(s).
[HttpGet]
- [MapToApiVersion("1.0")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task Query(
+ public async Task QueryV20(
string? fetch,
[FromQuery] string[] filter,
[FromQuery] string[] sort,
int skip = 0,
int take = 10)
+ => await HandleRequest(fetch, filter, sort, skip, take);
+
+ private async Task HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
{
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
Attempt, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs
index b0242bea5d..76ce80898f 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdMediaApiController.cs
@@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByIdMediaApiController : MediaApiControllerBase
{
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
@@ -16,16 +17,27 @@ public class ByIdMediaApiController : MediaApiControllerBase
{
}
+ [HttpGet("item/{id:guid}")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task ById(Guid id)
+ => await HandleRequest(id);
+
///
/// Gets a media item by id.
///
/// The unique identifier of the media item.
/// The media item or not found result.
[HttpGet("item/{id:guid}")]
- [MapToApiVersion("1.0")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task ById(Guid id)
+ public async Task ByIdV20(Guid id)
+ => await HandleRequest(id);
+
+ private async Task HandleRequest(Guid id)
{
IPublishedContent? media = PublishedMediaCache.GetById(id);
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs
index a9421eaa0c..8a3f4c7ceb 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByIdsMediaApiController.cs
@@ -10,6 +10,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByIdsMediaApiController : MediaApiControllerBase
{
public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
@@ -17,15 +18,25 @@ public class ByIdsMediaApiController : MediaApiControllerBase
{
}
+ [HttpGet("item")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task Item([FromQuery(Name = "id")] HashSet ids)
+ => await HandleRequest(ids);
+
///
/// Gets media items by ids.
///
/// The unique identifiers of the media items to retrieve.
/// The media items.
- [HttpGet("item")]
- [MapToApiVersion("1.0")]
+ [HttpGet("items")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
- public async Task Item([FromQuery(Name = "id")] HashSet ids)
+ public async Task ItemsV20([FromQuery(Name = "id")] HashSet ids)
+ => await HandleRequest(ids);
+
+ private async Task HandleRequest(HashSet ids)
{
IPublishedContent[] mediaItems = ids
.Select(PublishedMediaCache.GetById)
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs
index 1d725ac5ab..ed5dc90187 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/ByPathMediaApiController.cs
@@ -10,6 +10,7 @@ using Umbraco.Cms.Infrastructure.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class ByPathMediaApiController : MediaApiControllerBase
{
private readonly IApiMediaQueryService _apiMediaQueryService;
@@ -21,16 +22,27 @@ public class ByPathMediaApiController : MediaApiControllerBase
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
+ [HttpGet("item/{*path}")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task ByPath(string path)
+ => await HandleRequest(path);
+
///
/// Gets a media item by its path.
///
/// The path of the media item.
/// The media item or not found result.
[HttpGet("item/{*path}")]
- [MapToApiVersion("1.0")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task ByPath(string path)
+ public async Task ByPathV20(string path)
+ => await HandleRequest(path);
+
+ private async Task HandleRequest(string path)
{
path = DecodePath(path);
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs
index 5d962ea4bf..de872e5d86 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/QueryMediaApiController.cs
@@ -15,6 +15,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("1.0")]
+[ApiVersion("2.0")]
public class QueryMediaApiController : MediaApiControllerBase
{
private readonly IApiMediaQueryService _apiMediaQueryService;
@@ -26,6 +27,19 @@ public class QueryMediaApiController : MediaApiControllerBase
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
+ [HttpGet]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [Obsolete("Please use version 2 of this API. Will be removed in V15.")]
+ public async Task Query(
+ string? fetch,
+ [FromQuery] string[] filter,
+ [FromQuery] string[] sort,
+ int skip = 0,
+ int take = 10)
+ => await HandleRequest(fetch, filter, sort, skip, take);
+
///
/// Gets a paginated list of media item(s) from query.
///
@@ -36,15 +50,18 @@ public class QueryMediaApiController : MediaApiControllerBase
/// The amount of items to take.
/// The paged result of the media item(s).
[HttpGet]
- [MapToApiVersion("1.0")]
+ [MapToApiVersion("2.0")]
[ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
- public async Task Query(
+ public async Task QueryV20(
string? fetch,
[FromQuery] string[] filter,
[FromQuery] string[] sort,
int skip = 0,
int take = 10)
+ => await HandleRequest(fetch, filter, sort, skip, take);
+
+ private async Task HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
{
Attempt, ApiMediaQueryOperationStatus> queryAttempt = _apiMediaQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
index 86c8583708..f17fc14773 100644
--- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -1,5 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.DependencyInjection;
@@ -24,7 +26,22 @@ public static class UmbracoBuilderExtensions
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
{
builder.Services.AddScoped();
- builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped(provider =>
+ {
+ HttpContext? httpContext = provider.GetRequiredService().HttpContext;
+ ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
+ if (apiVersion is null)
+ {
+ return provider.GetRequiredService();
+ }
+
+ // V1 of the Delivery API uses a different expansion strategy than V2+
+ return apiVersion.MajorVersion == 1
+ ? provider.GetRequiredService()
+ : provider.GetRequiredService();
+ });
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs
index 7c42a3d0aa..9d938cef41 100644
--- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerContentDocumentationFilter.cs
@@ -15,7 +15,9 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
{
operation.Parameters ??= new List();
- AddExpand(operation);
+ AddExpand(operation, context);
+
+ AddFields(operation, context);
operation.Parameters.Add(new OpenApiParameter
{
diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
index 36721cc0f2..52acddaca9 100644
--- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
@@ -37,8 +37,52 @@ internal abstract class SwaggerDocumentationFilterBase
parameter.Examples = examples;
}
- protected void AddExpand(OpenApiOperation operation) =>
+ protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
+ {
+ if (IsApiV1(context))
+ {
+ AddExpandV1(operation);
+ }
+ else
+ {
+ AddExpand(operation);
+ }
+ }
+
+ protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
+ {
+ if (IsApiV1(context))
+ {
+ // "fields" is not a thing in Delivery API V1
+ return;
+ }
+
+ AddFields(operation);
+ }
+
+ protected void AddApiKey(OpenApiOperation operation) =>
operation.Parameters.Add(new OpenApiParameter
+ {
+ Name = "Api-Key",
+ In = ParameterLocation.Header,
+ Required = false,
+ Description = "API key specified through configuration to authorize access to the API.",
+ Schema = new OpenApiSchema { Type = "string" }
+ });
+
+ protected string PaginationDescription(bool skip, string itemType)
+ => $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
+
+ private string QueryParameterDescription(string description)
+ => $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
+
+ // FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
+ private static bool IsApiV1(OperationFilterContext context)
+ => context.ApiDescription.RelativePath?.Contains("api/v1") is true;
+
+ // FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
+ private void AddExpandV1(OpenApiOperation operation)
+ => operation.Parameters.Add(new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
@@ -60,19 +104,56 @@ internal abstract class SwaggerDocumentationFilterBase
}
});
- protected void AddApiKey(OpenApiOperation operation) =>
- operation.Parameters.Add(new OpenApiParameter
+ private void AddExpand(OpenApiOperation operation)
+ => operation.Parameters.Add(new OpenApiParameter
{
- Name = "Api-Key",
- In = ParameterLocation.Header,
+ Name = "expand",
+ In = ParameterLocation.Query,
Required = false,
- Description = "API key specified through configuration to authorize access to the API.",
- Schema = new OpenApiSchema { Type = "string" }
+ Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
+ Schema = new OpenApiSchema { Type = "string" },
+ Examples = new Dictionary
+ {
+ { "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
+ { "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
+ {
+ "Expand specific property",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
+ },
+ {
+ "Expand specific properties",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
+ },
+ {
+ "Expand nested properties",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
+ }
+ }
});
- protected string PaginationDescription(bool skip, string itemType)
- => $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
-
- private string QueryParameterDescription(string description)
- => $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
+ private void AddFields(OpenApiOperation operation)
+ => operation.Parameters.Add(new OpenApiParameter
+ {
+ Name = "fields",
+ In = ParameterLocation.Query,
+ Required = false,
+ Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
+ Schema = new OpenApiSchema { Type = "string" },
+ Examples = new Dictionary
+ {
+ { "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
+ {
+ "Include only specific property",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
+ },
+ {
+ "Include only specific properties",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
+ },
+ {
+ "Include only specific nested properties",
+ new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
+ }
+ }
+ });
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs
index 8529178888..85ba66e648 100644
--- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerMediaDocumentationFilter.cs
@@ -15,7 +15,9 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
{
operation.Parameters ??= new List();
- AddExpand(operation);
+ AddExpand(operation, context);
+
+ AddFields(operation, context);
AddApiKey(operation);
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs
new file mode 100644
index 0000000000..6cc2100e93
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs
@@ -0,0 +1,185 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Delivery.Rendering;
+
+internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
+{
+ private const string All = "$all";
+ private const string None = "";
+ private const string ExpandParameterName = "expand";
+ private const string FieldsParameterName = "fields";
+
+ private readonly IApiPropertyRenderer _propertyRenderer;
+ private readonly ILogger _logger;
+
+ private readonly Stack _expandProperties;
+ private readonly Stack _includeProperties;
+
+ public RequestContextOutputExpansionStrategyV2(
+ IHttpContextAccessor httpContextAccessor,
+ IApiPropertyRenderer propertyRenderer,
+ ILogger logger)
+ {
+ _propertyRenderer = propertyRenderer;
+ _logger = logger;
+ _expandProperties = new Stack();
+ _includeProperties = new Stack();
+
+ InitializeExpandAndInclude(httpContextAccessor);
+ }
+
+ public IDictionary MapContentProperties(IPublishedContent content)
+ => content.ItemType == PublishedItemType.Content
+ ? MapProperties(content.Properties)
+ : throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
+
+ public IDictionary MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
+ {
+ if (media.ItemType != PublishedItemType.Media)
+ {
+ throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
+ }
+
+ IPublishedProperty[] properties = media
+ .Properties
+ .Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
+ .ToArray();
+
+ return properties.Any()
+ ? MapProperties(properties)
+ : new Dictionary();
+ }
+
+ public IDictionary MapElementProperties(IPublishedElement element)
+ => MapProperties(element.Properties, true);
+
+ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
+ {
+ string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
+
+ var toExpand = QueryValue(ExpandParameterName) ?? None;
+ var toInclude = QueryValue(FieldsParameterName) ?? All;
+
+ try
+ {
+ _expandProperties.Push(Node.Parse(toExpand));
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.LogError(ex, $"Could not parse the '{ExpandParameterName}' parameter. See exception for details.");
+ throw new ArgumentException($"Could not parse the '{ExpandParameterName}' parameter: {ex.Message}");
+ }
+
+ try
+ {
+ _includeProperties.Push(Node.Parse(toInclude));
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.LogError(ex, $"Could not parse the '{FieldsParameterName}' parameter. See exception for details.");
+ throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
+ }
+ }
+
+ private IDictionary MapProperties(IEnumerable properties, bool forceExpandProperties = false)
+ {
+ Node? currentExpandProperties = _expandProperties.Peek();
+ if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
+ {
+ return new Dictionary();
+ }
+
+ Node? currentIncludeProperties = _includeProperties.Peek();
+ var result = new Dictionary();
+ foreach (IPublishedProperty property in properties)
+ {
+ Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
+ if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
+ {
+ continue;
+ }
+
+ Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
+
+ _includeProperties.Push(nextIncludeProperties);
+ _expandProperties.Push(nextExpandProperties);
+
+ result[property.Alias] = GetPropertyValue(property);
+
+ _expandProperties.Pop();
+ _includeProperties.Pop();
+ }
+
+ return result;
+ }
+
+ private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
+ => currentProperties?.Items.FirstOrDefault(i => i.Key == All)
+ ?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
+
+ private object? GetPropertyValue(IPublishedProperty property)
+ => _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
+
+ private class Node
+ {
+ public string Key { get; private set; } = string.Empty;
+
+ public List Items { get; } = new();
+
+ public static Node Parse(string value)
+ {
+ // verify that there are as many start brackets as there are end brackets
+ if (value.CountOccurrences("[") != value.CountOccurrences("]"))
+ {
+ throw new ArgumentException("Value did not contain an equal number of start and end brackets");
+ }
+
+ // verify that the value does not start with a start bracket
+ if (value.StartsWith("["))
+ {
+ throw new ArgumentException("Value cannot start with a bracket");
+ }
+
+ // verify that there are no empty brackets
+ if (value.Contains("[]"))
+ {
+ throw new ArgumentException("Value cannot contain empty brackets");
+ }
+
+ var stack = new Stack();
+ var root = new Node { Key = "root" };
+ stack.Push(root);
+
+ var currentNode = new Node();
+ root.Items.Add(currentNode);
+
+ foreach (char c in value)
+ {
+ switch (c)
+ {
+ case '[': // Start a new node, child of the current node
+ stack.Push(currentNode);
+ currentNode = new Node();
+ stack.Peek().Items.Add(currentNode);
+ break;
+ case ',': // Start a new node, but at the same level of the current node
+ currentNode = new Node();
+ stack.Peek().Items.Add(currentNode);
+ break;
+ case ']': // Back to parent of the current node
+ currentNode = stack.Pop();
+ break;
+ default: // Add char to current node key
+ currentNode.Key += c;
+ break;
+ }
+ }
+
+ return root;
+ }
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs b/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs
index 52b38414f8..186cd4f555 100644
--- a/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs
@@ -1,3 +1,4 @@
+using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
@@ -29,9 +30,25 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
+ ApiVersion? requestedApiVersion = httpContext.GetRequestedApiVersion();
for (var i = 0; i < candidates.Count; i++)
{
- ControllerActionDescriptor? controllerActionDescriptor = candidates[i].Endpoint?.Metadata.GetMetadata();
+ CandidateState candidate = candidates[i];
+ Endpoint? endpoint = candidate.Endpoint;
+
+ // NOTE: nullability for the CandidateState.Endpoint property is not correct - it *can* be null
+ if (endpoint is null)
+ {
+ continue;
+ }
+
+ if (EndpointSupportsApiVersion(endpoint, requestedApiVersion) is false)
+ {
+ candidates.SetValidity(i, false);
+ continue;
+ }
+
+ ControllerActionDescriptor? controllerActionDescriptor = endpoint.Metadata.GetMetadata();
if (IsByIdsController(controllerActionDescriptor))
{
candidates.SetValidity(i, hasIdQueryParameter);
@@ -45,6 +62,15 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
return Task.CompletedTask;
}
+ private static bool EndpointSupportsApiVersion(Endpoint endpoint, ApiVersion? requestedApiVersion)
+ {
+ ApiVersion[]? supportedApiVersions = endpoint.Metadata.GetMetadata()?.Versions.ToArray();
+
+ // if the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
+ // if the endpoint is NOT versioned, it cannot be used with a requested API version
+ return supportedApiVersions?.Contains(requestedApiVersion) ?? requestedApiVersion is null;
+ }
+
private static bool IsByIdsController(ControllerActionDescriptor? controllerActionDescriptor)
=> IsControllerType(controllerActionDescriptor) || IsControllerType(controllerActionDescriptor);
diff --git a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
index 0306dab5af..8948a49a1a 100644
--- a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
+++ b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
@@ -13,5 +13,8 @@
<_Parameter1>Umbraco.Tests.UnitTests
+
+ <_Parameter1>DynamicProxyGenAssembly2
+
diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs
index e7849eef12..732e25ecc2 100644
--- a/src/Umbraco.Core/Extensions/StringExtensions.cs
+++ b/src/Umbraco.Core/Extensions/StringExtensions.cs
@@ -419,7 +419,7 @@ public static class StringExtensions
/// returns .
///
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
-
+
[return: NotNullIfNotNull("defaultValue")]
public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) =>
str.IsNullOrWhiteSpace() ? defaultValue : str;
@@ -1557,4 +1557,9 @@ public static class StringExtensions
yield return sb.ToString();
}
+
+ // having benchmarked various solutions (incl. for/foreach, split and LINQ based ones),
+ // this is by far the fastest way to find string needles in a string haystack
+ public static int CountOccurrences(this string haystack, string needle)
+ => haystack.Length - haystack.Replace(needle, string.Empty).Length;
}
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
index 45d36abb6a..4cf5bdd6af 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
@@ -56,6 +56,12 @@ public interface IPublishedPropertyType
///
PropertyCacheLevel DeliveryApiCacheLevel { get; }
+ ///
+ /// Gets the property cache level for Delivery API representation when expanding the property.
+ ///
+ /// Defaults to the value of .
+ PropertyCacheLevel DeliveryApiCacheLevelForExpansion => DeliveryApiCacheLevel;
+
///
/// Gets the property model CLR type.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
index 848e961d0b..52e3371767 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
@@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
private IPropertyValueConverter? _converter;
private PropertyCacheLevel _cacheLevel;
private PropertyCacheLevel _deliveryApiCacheLevel;
+ private PropertyCacheLevel _deliveryApiCacheLevelForExpansion;
private Type? _modelClrType;
private Type? _clrType;
@@ -192,9 +193,15 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
}
_cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Snapshot;
- _deliveryApiCacheLevel = _converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter
- ? deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevel(this)
- : _cacheLevel;
+ if (_converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter)
+ {
+ _deliveryApiCacheLevel = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevel(this);
+ _deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevelForExpansion(this);
+ }
+ else
+ {
+ _deliveryApiCacheLevel = _deliveryApiCacheLevelForExpansion = _cacheLevel;
+ }
_modelClrType = _converter?.GetPropertyValueType(this) ?? typeof(object);
}
@@ -244,6 +251,20 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
}
}
+ ///
+ public PropertyCacheLevel DeliveryApiCacheLevelForExpansion
+ {
+ get
+ {
+ if (!_initialized)
+ {
+ Initialize();
+ }
+
+ return _deliveryApiCacheLevelForExpansion;
+ }
+ }
+
///
public object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview)
{
diff --git a/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs
index 51d9f95873..4d539d95ce 100644
--- a/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs
@@ -11,6 +11,15 @@ public interface IDeliveryApiPropertyValueConverter : IPropertyValueConverter
/// The property cache level.
PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType);
+ ///
+ /// Gets the property cache level for Delivery API representation when expanding the property.
+ ///
+ /// The property type.
+ /// The property cache level.
+ /// Defaults to the value of .
+ PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType)
+ => GetDeliveryApiPropertyCacheLevel(propertyType);
+
///
/// Gets the type of values returned by the converter for Delivery API representation.
///
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs
index 94f2533548..e13f4c3e45 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs
@@ -99,6 +99,8 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IApiContent);
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs
index 468d4cdd0c..9c7c481752 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs
@@ -124,6 +124,8 @@ public class MediaPickerValueConverter : PropertyValueConverterBase, IDeliveryAp
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable);
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
index c14a56a0bd..7d512fbb2b 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
@@ -183,6 +183,8 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> GetEntityType(propertyType) switch
{
diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
index 05348a138c..09f63a8254 100644
--- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
+++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs
@@ -89,6 +89,9 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase
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
@@ -223,7 +226,15 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
{
- GetDeliveryApiCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
+ PropertyCacheLevel cacheLevel, referenceCacheLevel;
+ if (expanding)
+ {
+ GetDeliveryApiCacheLevelsForExpansion(out cacheLevel, out referenceCacheLevel);
+ }
+ else
+ {
+ GetDeliveryApiCacheLevels(out cacheLevel, out referenceCacheLevel);
+ }
lock (_locko)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs
index 5b877bd9b9..d0e1e2ba19 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs
@@ -52,6 +52,10 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(BlockGridModel);
+ ///
+ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
+ => PropertyCacheLevel.Element;
+
///
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
=> ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview);
@@ -59,6 +63,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
///
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
+ ///
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
///
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(ApiBlockGridModel);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
index 4c65963093..03445094c1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
@@ -111,6 +111,9 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
///
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
+ ///
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
///
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(ApiBlockListModel);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
index 817d48687a..8a22ca92fe 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
@@ -151,6 +151,8 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable);
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs
index 649fbf36df..871595b3bc 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs
@@ -144,6 +144,8 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
+ public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> _deliveryApiSettings.RichTextOutputAsJson
? typeof(IRichTextElement)
diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs
index 6a9e1a982c..2892a04f90 100644
--- a/src/Umbraco.PublishedCache.NuCache/Property.cs
+++ b/src/Umbraco.PublishedCache.NuCache/Property.cs
@@ -323,7 +323,7 @@ internal class Property : PublishedPropertyBase
object? value;
lock (_locko)
{
- CacheValue cacheValues = GetCacheValues(PropertyType.DeliveryApiCacheLevel).For(culture, segment);
+ CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment);
// initial reference cache level always is .Content
const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element;
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs
index 750be34ea1..7c13e5780d 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs
@@ -49,6 +49,7 @@ public class CacheTests
var invocationCount = 0;
propertyType.SetupGet(p => p.CacheLevel).Returns(cacheLevel);
propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(cacheLevel);
+ propertyType.SetupGet(p => p.DeliveryApiCacheLevelForExpansion).Returns(cacheLevel);
propertyType
.Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), It.IsAny(), It.IsAny