From a4c7047a5059ea6a4975ef68a9b5599cee11affb Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 20 Nov 2023 11:01:36 +0100 Subject: [PATCH] Add output caching to the Delivery API (#15216) --- .../Caching/DeliveryApiOutputCachePolicy.cs | 32 ++++++++++++++ .../Caching/OutputCachePipelineFilter.cs | 14 ++++++ .../Content/ContentApiControllerBase.cs | 3 ++ .../Media/MediaApiControllerBase.cs | 3 ++ .../UmbracoBuilderExtensions.cs | 37 +++++++++++++++- .../Models/DeliveryApiSettings.cs | 43 +++++++++++++++++++ src/Umbraco.Core/Constants-DeliveryApi.cs | 16 +++++++ 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs create mode 100644 src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs new file mode 100644 index 0000000000..da1580554c --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Delivery.Caching; + +internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy +{ + private readonly TimeSpan _duration; + + public DeliveryApiOutputCachePolicy(TimeSpan duration) + => _duration = duration; + + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) + { + IRequestPreviewService requestPreviewService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + context.EnableOutputCaching = requestPreviewService.IsPreview() is false; + context.ResponseExpirationTimeSpan = _duration; + + return ValueTask.CompletedTask; + } + + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; + + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs b/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs new file mode 100644 index 0000000000..89ff10462c --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Caching/OutputCachePipelineFilter.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Builder; +using Umbraco.Cms.Web.Common.ApplicationBuilder; + +namespace Umbraco.Cms.Api.Delivery.Caching; + +internal sealed class OutputCachePipelineFilter : UmbracoPipelineFilter +{ + public OutputCachePipelineFilter(string name) + : base(name) + => PostPipeline = PostPipelineAction; + + private void PostPipelineAction(IApplicationBuilder applicationBuilder) + => applicationBuilder.UseOutputCache(); +} diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs index 405da6e15f..4637055c11 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Delivery.Filters; using Umbraco.Cms.Api.Delivery.Routing; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Services.OperationStatus; @@ -13,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content; [ApiExplorerSettings(GroupName = "Content")] [LocalizeFromAcceptLanguageHeader] [ValidateStartItem] +[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)] public abstract class ContentApiControllerBase : DeliveryApiControllerBase { protected IApiPublishedContentCache ApiPublishedContentCache { get; } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs index 73a385fd2e..5a9bc4763e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Media/MediaApiControllerBase.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Delivery.Filters; using Umbraco.Cms.Api.Delivery.Routing; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -14,6 +16,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media; [DeliveryApiMediaAccess] [VersionedDeliveryApiRoute("media")] [ApiExplorerSettings(GroupName = "Media")] +[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.MediaCachePolicy)] public abstract class MediaApiControllerBase : DeliveryApiControllerBase { private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder; diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index f17fc14773..9f56f591ce 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -3,9 +3,11 @@ using System.Text.Json.Serialization; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.DependencyInjection; using Umbraco.Cms.Api.Delivery.Accessors; +using Umbraco.Cms.Api.Delivery.Caching; using Umbraco.Cms.Api.Delivery.Configuration; using Umbraco.Cms.Api.Delivery.Handlers; using Umbraco.Cms.Api.Delivery.Json; @@ -14,10 +16,12 @@ using Umbraco.Cms.Api.Delivery.Routing; using Umbraco.Cms.Api.Delivery.Security; using Umbraco.Cms.Api.Delivery.Services; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Cms.Web.Common.ApplicationBuilder; namespace Umbraco.Extensions; @@ -79,7 +83,38 @@ public static class UmbracoBuilderExtensions // FIXME: remove this when Delivery API V1 is removed builder.Services.AddSingleton(); + + builder.AddOutputCache(); + return builder; + } + + private static IUmbracoBuilder AddOutputCache(this IUmbracoBuilder builder) + { + DeliveryApiSettings.OutputCacheSettings outputCacheSettings = + builder.Config.GetSection(Constants.Configuration.ConfigDeliveryApi).Get()?.OutputCache + ?? new DeliveryApiSettings.OutputCacheSettings(); + + if (outputCacheSettings.Enabled is false || outputCacheSettings is { ContentDuration.TotalSeconds: <= 0, MediaDuration.TotalSeconds: <= 0 }) + { + return builder; + } + + builder.Services.AddOutputCache(options => + { + options.AddBasePolicy(_ => { }); + + if (outputCacheSettings.ContentDuration.TotalSeconds > 0) + { + options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration)); + } + + if (outputCacheSettings.MediaDuration.TotalSeconds > 0) + { + options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration)); + } + }); + + builder.Services.Configure(options => options.AddFilter(new OutputCachePipelineFilter("UmbracoDeliveryApiOutputCache"))); return builder; } } - diff --git a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs index c81102b8d2..1f75d47055 100644 --- a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs @@ -59,6 +59,11 @@ public class DeliveryApiSettings /// public MemberAuthorizationSettings? MemberAuthorization { get; set; } = null; + /// + /// Gets or sets the settings for the Delivery API output cache. + /// + public OutputCacheSettings OutputCache { get; set; } = new (); + /// /// Gets a value indicating if any member authorization type is enabled for the Delivery API. /// @@ -138,4 +143,42 @@ public class DeliveryApiSettings /// These are only required if logout is to be used. public Uri[] LogoutRedirectUrls { get; set; } = Array.Empty(); } + + /// + /// Typed configuration options for output caching of the Delivery API. + /// + public class OutputCacheSettings + { + private const string StaticDuration = "00:01:00"; // one minute + + /// + /// Gets or sets a value indicating whether the Delivery API output should be cached. + /// + /// true if the Delivery API output should be cached; otherwise, false. + /// + /// The default value is false. + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; + + /// + /// Gets or sets a value indicating how long the Content Delivery API output should be cached. + /// + /// Cache lifetime. + /// + /// The default cache duration is one minute, if this configuration value is not provided. + /// + [DefaultValue(StaticDuration)] + public TimeSpan ContentDuration { get; set; } = TimeSpan.Parse(StaticDuration); + + /// + /// Gets or sets a value indicating how long the Media Delivery API output should be cached. + /// + /// Cache lifetime. + /// + /// The default cache duration is one minute, if this configuration value is not provided. + /// + [DefaultValue(StaticDuration)] + public TimeSpan MediaDuration { get; set; } = TimeSpan.Parse(StaticDuration); + } } diff --git a/src/Umbraco.Core/Constants-DeliveryApi.cs b/src/Umbraco.Core/Constants-DeliveryApi.cs index e2f23e414c..85677a23bc 100644 --- a/src/Umbraco.Core/Constants-DeliveryApi.cs +++ b/src/Umbraco.Core/Constants-DeliveryApi.cs @@ -17,5 +17,21 @@ public static partial class Constants /// public const string PreviewContentPathPrefix = "preview-"; } + + /// + /// Constants for Delivery API output cache. + /// + public static class OutputCache + { + /// + /// Output cache policy name for content + /// + public const string ContentCachePolicy = "DeliveryApiContent"; + + /// + /// Output cache policy name for media + /// + public const string MediaCachePolicy = "DeliveryApiMedia"; + } } }