diff --git a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs index 5907e1e13b..e53727b152 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -13,6 +15,7 @@ namespace Umbraco.Cms.Core.Cache; public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { private readonly IOptions _options; + private readonly IHostEnvironment? _hostEnvironment; private readonly ISet _keys = new HashSet(); private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion); private bool _disposedValue; @@ -29,7 +32,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable /// Initializes a new instance of the . /// public ObjectCacheAppCache() - : this(Options.Create(new MemoryCacheOptions()), NullLoggerFactory.Instance) + : this(Options.Create(new MemoryCacheOptions()), NullLoggerFactory.Instance, null) { } /// @@ -37,9 +40,11 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable /// /// The options. /// The logger factory. - public ObjectCacheAppCache(IOptions options, ILoggerFactory loggerFactory) + /// The host environment. + public ObjectCacheAppCache(IOptions options, ILoggerFactory loggerFactory, IHostEnvironment? hostEnvironment) { _options = options; + _hostEnvironment = hostEnvironment; MemoryCache = new MemoryCache(_options, loggerFactory); } @@ -101,7 +106,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable } /// - public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false) + public object? Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, string[]? dependentFiles = null) { // see notes in HttpRuntimeAppCache Lazy? result; @@ -116,7 +121,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) { result = SafeLazy.GetSafeLazy(factory); - MemoryCacheEntryOptions options = GetOptions(timeout, isSliding); + MemoryCacheEntryOptions options = GetOptions(timeout, isSliding, dependentFiles); try { @@ -154,7 +159,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable } /// - public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false) + public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) { // NOTE - here also we must insert a Lazy but we can evaluate it right now // and make sure we don't store a null value. @@ -165,7 +170,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable return; // do not store null values (backward compat) } - MemoryCacheEntryOptions options = GetOptions(timeout, isSliding); + MemoryCacheEntryOptions options = GetOptions(timeout, isSliding, dependentFiles); // NOTE: This does an add or update MemoryCache.Set(key, result, options); @@ -318,7 +323,7 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable } } - private MemoryCacheEntryOptions GetOptions(TimeSpan? timeout, bool isSliding) + private MemoryCacheEntryOptions GetOptions(TimeSpan? timeout = null, bool isSliding = false, string[]? dependentFiles = null) { var options = new MemoryCacheEntryOptions(); @@ -332,6 +337,19 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable options.AbsoluteExpirationRelativeToNow = timeout; } + // Configure file based expiration + if (dependentFiles?.Length > 0 && _hostEnvironment?.ContentRootFileProvider is IFileProvider fileProvider) + { + foreach (var dependentFile in dependentFiles) + { + var relativePath = Path.IsPathFullyQualified(dependentFile) + ? Path.GetRelativePath(_hostEnvironment.ContentRootPath, dependentFile) + : dependentFile; + + options.ExpirationTokens.Add(fileProvider.Watch(relativePath)); + } + } + // Ensure key is removed from set when evicted from cache return options.RegisterPostEvictionCallback((key, _, _, _) => _keys.Remove((string)key)); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 24a0663d83..4c80fc55d7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -17,6 +17,19 @@ + + + + + + + + + + + + + diff --git a/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs index c65f687e16..33ddbf5bc4 100644 --- a/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/OpenIddictCleanup.cs @@ -1,4 +1,3 @@ -using System.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenIddict.Abstractions; @@ -41,8 +40,7 @@ public class OpenIddictCleanup : RecurringHostedServiceBase try { - IOpenIddictTokenManager tokenManager = scope.ServiceProvider.GetService() - ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictTokenManager)} service from the current scope"); + IOpenIddictTokenManager tokenManager = scope.ServiceProvider.GetRequiredService(); await tokenManager.PruneAsync(threshold); } catch (Exception exception) @@ -52,8 +50,7 @@ public class OpenIddictCleanup : RecurringHostedServiceBase try { - IOpenIddictAuthorizationManager authorizationManager = scope.ServiceProvider.GetService() - ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictAuthorizationManager)} service from the current scope"); + IOpenIddictAuthorizationManager authorizationManager = scope.ServiceProvider.GetRequiredService(); await authorizationManager.PruneAsync(threshold); } catch (Exception exception) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DeepCloneAppCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DeepCloneAppCacheTests.cs index 4eed8e0d41..e2cfb09a6b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DeepCloneAppCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DeepCloneAppCacheTests.cs @@ -2,7 +2,6 @@ // See LICENSE for more details. using System.Diagnostics; -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Collections; @@ -27,7 +26,7 @@ public class DeepCloneAppCacheTests : RuntimeAppCacheTests private DeepCloneAppCache _provider; private ObjectCacheAppCache _memberCache; - protected override int GetTotalItemCount => _memberCache.MemoryCache.Count(); + protected override int GetTotalItemCount => _memberCache.MemoryCache.Count; internal override IAppCache AppCache => _provider; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ObjectAppCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ObjectAppCacheTests.cs index 4b68a85871..e2d9143771 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ObjectAppCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/ObjectAppCacheTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; @@ -18,7 +17,7 @@ public class ObjectAppCacheTests : RuntimeAppCacheTests private ObjectCacheAppCache _provider; - protected override int GetTotalItemCount => _provider.MemoryCache.Count(); + protected override int GetTotalItemCount => _provider.MemoryCache.Count; internal override IAppCache AppCache => _provider;