Adds variation by the header name Accept-Language to the delivery API output cache policy (#19709)

* Adds variation by the header name Accept-Language to the develivery API output cache policy

* Removed obsolete constructor (not necessary as the class is internal).

* Introduce contants for header names.
This commit is contained in:
Andy Butland
2025-07-11 15:51:05 +02:00
parent 543b644ed1
commit 4deb756e64
9 changed files with 67 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Caching;
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
{
private readonly TimeSpan _duration;
private readonly StringValues _varyByHeaderNames;
public DeliveryApiOutputCachePolicy(TimeSpan duration)
=> _duration = duration;
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
{
_duration = duration;
_varyByHeaderNames = varyByHeaderNames;
}
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
@@ -20,6 +25,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
return ValueTask.CompletedTask;
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
using Umbraco.Cms.Api.Delivery.Caching;
@@ -110,12 +111,20 @@ public static class UmbracoBuilderExtensions
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.ContentDuration,
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem])));
}
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.MediaDuration,
Constants.DeliveryApi.HeaderNames.StartItem));
}
});

View File

@@ -1,4 +1,4 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Language",
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
@@ -35,7 +35,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Segment",
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
@@ -51,7 +51,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Preview",
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
@@ -60,7 +60,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Start-Item",
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
protected void AddApiKey(OpenApiOperation operation) =>
operation.Parameters.Add(new OpenApiParameter
{
Name = "Api-Key",
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
}

View File

@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
}
/// <inheritdoc />
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
}

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
@@ -12,5 +12,5 @@ internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegm
/// <inheritdoc />
public string? GetRequestedSegment()
=> GetHeaderValue("Accept-Segment");
=> GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.AcceptSegment);
}

View File

@@ -60,5 +60,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
}
/// <inheritdoc/>
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
}

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core;
namespace Umbraco.Cms.Core;
public static partial class Constants
{
@@ -24,14 +24,45 @@ public static partial class Constants
public static class OutputCache
{
/// <summary>
/// Output cache policy name for content
/// Output cache policy name for content.
/// </summary>
public const string ContentCachePolicy = "DeliveryApiContent";
/// <summary>
/// Output cache policy name for media
/// Output cache policy name for media.
/// </summary>
public const string MediaCachePolicy = "DeliveryApiMedia";
}
/// <summary>
/// Constants for Delivery API header names.
/// </summary>
public static class HeaderNames
{
/// <summary>
/// Header name for accept language.
/// </summary>
public const string AcceptLanguage = "Accept-Language";
/// <summary>
/// Header name for accept segment.
/// </summary>
public const string AcceptSegment = "Accept-Segment";
/// <summary>
/// Header name for API key.
/// </summary>
public const string ApiKey = "Api-Key";
/// <summary>
/// Header name for preview.
/// </summary>
public const string Preview = "Preview";
/// <summary>
/// Header name for start item.
/// </summary>
public const string StartItem = "Start-Item";
}
}
}