diff --git a/src/Umbraco.Core/Cache/ICacheSyncService.cs b/src/Umbraco.Core/Cache/ICacheSyncService.cs
new file mode 100644
index 0000000000..12da6aa82e
--- /dev/null
+++ b/src/Umbraco.Core/Cache/ICacheSyncService.cs
@@ -0,0 +1,33 @@
+namespace Umbraco.Cms.Core.Cache;
+
+///
+/// Provides cache synchronization capabilities for load-balanced Umbraco environments.
+///
+///
+/// This service synchronizes isolated caches across servers in a load-balanced cluster by rolling forward
+/// out-of-date caches. It separates synchronization into two distinct operations: internal isolated caches
+/// (repositories and services) and published content caches, enabling selective cache refreshing.
+///
+public interface ICacheSyncService
+{
+ ///
+ /// Synchronizes all caches including both isolated caches and published content caches.
+ ///
+ /// A token to monitor for cancellation requests.
+ ///
+ /// This method clears all isolated caches (repositories and services) and published content caches
+ /// (IPublishedContentCache, route caching, etc.) to ensure complete cache consistency across the cluster.
+ ///
+ void SyncAll(CancellationToken cancellationToken);
+
+ ///
+ /// Synchronizes only isolated caches without affecting the published content cache layer.
+ ///
+ /// A token to monitor for cancellation requests.
+ ///
+ /// This method clears only the isolated caches used by repositories and services, leaving the
+ /// published content cache layer intact. During synchronization, repositories reload data from
+ /// the database while temporarily bypassing version checking to prevent recursive sync attempts.
+ ///
+ void SyncInternal(CancellationToken cancellationToken);
+}
diff --git a/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs b/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs
new file mode 100644
index 0000000000..61ae733fe1
--- /dev/null
+++ b/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs
@@ -0,0 +1,33 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Cache;
+
+///
+/// Provides access to repository cache version information with request-level caching.
+///
+///
+/// This accessor retrieves cache version information from the database and caches it at the request level
+/// to minimize database queries. Cache versions are used to determine if cached repository data is still valid
+/// in distributed environments.
+///
+public interface IRepositoryCacheVersionAccessor
+{
+
+ ///
+ /// Retrieves the cache version for the specified cache key.
+ ///
+ /// The unique identifier for the cache entry.
+ ///
+ /// The cache version if found, or if the version doesn't exist or the request is a client-side request.
+ ///
+ public Task GetAsync(string cacheKey);
+
+ ///
+ /// Notifies the accessor that caches have been synchronized.
+ ///
+ ///
+ /// This method is called after cache synchronization to temporarily bypass version checking,
+ /// preventing recursive sync attempts while repositories reload data from the database.
+ ///
+ public void CachesSynced();
+}
diff --git a/src/Umbraco.Core/Cache/IRepositoryCacheVersionService.cs b/src/Umbraco.Core/Cache/IRepositoryCacheVersionService.cs
new file mode 100644
index 0000000000..7acc00cee5
--- /dev/null
+++ b/src/Umbraco.Core/Cache/IRepositoryCacheVersionService.cs
@@ -0,0 +1,28 @@
+namespace Umbraco.Cms.Core.Cache;
+
+///
+/// Provides methods to manage and validate cache versioning for repository entities,
+/// ensuring cache consistency with the underlying database.
+///
+public interface IRepositoryCacheVersionService
+{
+ ///
+ /// Validates if the cache is synced with the database.
+ ///
+ /// The type of the cached entity.
+ /// True if cache is synced, false if cache needs fast-forwarding.
+ Task IsCacheSyncedAsync()
+ where TEntity : class;
+
+ ///
+ /// Registers a cache update for the specified entity type.
+ ///
+ /// The type of the cached entity.
+ Task SetCacheUpdatedAsync()
+ where TEntity : class;
+
+ ///
+ /// Registers that the cache has been synced with the database.
+ ///
+ Task SetCachesSyncedAsync();
+}
diff --git a/src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs
index d01bf617fd..8f3c1b057d 100644
--- a/src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/IJsonCacheRefresher.cs
@@ -10,4 +10,10 @@ public interface IJsonCacheRefresher : ICacheRefresher
///
///
void Refresh(string json);
+
+ ///
+ /// Refreshes internal (isolated) caches by a json payload.
+ ///
+ /// The json payload.
+ void RefreshInternal(string json) => Refresh(json);
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs
index 426481ea0a..9a0616a5c8 100644
--- a/src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/IPayloadCacheRefresher.cs
@@ -10,4 +10,10 @@ public interface IPayloadCacheRefresher : IJsonCacheRefresher
///
///
void Refresh(TPayload[] payloads);
+
+ ///
+ /// Refreshes internal (isolated) caches by a payload.
+ ///
+ /// The payload.
+ void RefreshInternal(TPayload[] payloads) => Refresh(payloads);
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
index fae5a03144..7505781ad9 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
@@ -87,7 +87,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase();
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
@@ -99,7 +99,6 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase();
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate();
foreach (JsonPayload payload in payloads)
@@ -119,14 +118,23 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase((k, v) => v.Path?.Contains(pathid) ?? false);
}
+ }
+ base.RefreshInternal(payloads);
+ }
+
+ public override void Refresh(JsonPayload[] payloads)
+ {
+ var idsRemoved = new HashSet();
+
+ foreach (JsonPayload payload in payloads)
+ {
// if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
{
idsRemoved.Add(payload.Id);
}
-
HandleMemoryCache(payload);
HandleRouting(payload);
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
index 35320f47d3..fc898e1be3 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs
@@ -76,7 +76,7 @@ public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase x.Id));
_publishedContentTypeFactory.NotifyDataTypeChanges();
_publishedModelFactory.WithSafeLiveFactoryReset(() =>
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
index bf45161caa..101be3d0ac 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs
@@ -72,7 +72,7 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase dataTypeCache = AppCaches.IsolatedCaches.Get();
- List removedContentTypes = new();
foreach (JsonPayload payload in payloads)
{
_idKeyMap.ClearCache(payload.Id);
@@ -95,7 +94,16 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase(payload.Id));
}
+ }
+ base.RefreshInternal(payloads);
+ }
+
+ public override void Refresh(JsonPayload[] payloads)
+ {
+ List removedContentTypes = new();
+ foreach (JsonPayload payload in payloads)
+ {
removedContentTypes.AddRange(_publishedContentTypeCache.ClearByDataTypeId(payload.Id));
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs
index 4c765cda71..55dd2444a3 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs
@@ -51,7 +51,7 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase();
@@ -61,8 +61,8 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase throw new NotSupportedException();
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
index 32d21704d2..45af4d2c8f 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
@@ -83,13 +83,8 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase mediaCache = AppCaches.IsolatedCaches.Get();
@@ -108,22 +103,37 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase(payload.Id));
- mediaCache.Result?.Clear(RepositoryCacheKeys.GetKey(payload.Key));
-
- // remove those that are in the branch
- if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
- {
- var pathid = "," + payload.Id + ",";
- mediaCache.Result?.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false);
- }
+ continue;
}
+ // repository cache
+ // it *was* done for each pathId but really that does not make sense
+ // only need to do it for the current media
+ mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
+ mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key));
+
+ // remove those that are in the branch
+ if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove))
+ {
+ var pathid = "," + payload.Id + ",";
+ mediaCache.Result.ClearOfType((_, v) => v.Path?.Contains(pathid) ?? false);
+ }
+ }
+
+ base.RefreshInternal(payloads);
+ }
+
+ public override void Refresh(JsonPayload[]? payloads)
+ {
+ if (payloads is null)
+ {
+ return;
+ }
+
+ foreach (JsonPayload payload in payloads)
+ {
HandleMemoryCache(payload);
HandleNavigation(payload);
}
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs
index de38b25d32..2a7b449aaf 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs
@@ -71,10 +71,10 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase "Member Cache Refresher";
- public override void Refresh(JsonPayload[] payloads)
+ public override void RefreshInternal(JsonPayload[] payloads)
{
ClearCache(payloads);
- base.Refresh(payloads);
+ base.RefreshInternal(payloads);
}
public override void Refresh(int id)
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs
index 05bd6049c8..0d5eaf0010 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MemberGroupCacheRefresher.cs
@@ -41,10 +41,10 @@ public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase "ValueEditorCacheRefresher";
- public override void Refresh(DataTypeCacheRefresher.JsonPayload[] payloads)
+ public override void RefreshInternal(DataTypeCacheRefresher.JsonPayload[] payloads)
{
IEnumerable ids = payloads.Select(x => x.Id);
_valueEditorCache.ClearCache(ids);
+ base.RefreshInternal(payloads);
}
// these events should never trigger
diff --git a/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
index f638ab34b0..52f0d7439c 100644
--- a/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
@@ -33,6 +33,11 @@ public abstract class JsonCacheRefresherBase : Cach
public virtual void Refresh(string json) =>
OnCacheUpdated(NotificationFactory.Create(json, MessageType.RefreshByJson));
+ ///
+ public virtual void RefreshInternal(string json)
+ {
+ }
+
#region Json
///
diff --git a/src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs
index f371e80979..8fc8e1e471 100644
--- a/src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/PayloadCacheRefresherBase.cs
@@ -39,6 +39,16 @@ public abstract class
}
}
+ ///
+ public override void RefreshInternal(string json)
+ {
+ TPayload[]? payload = Deserialize(json);
+ if (payload is not null)
+ {
+ RefreshInternal(payload);
+ }
+ }
+
///
/// Refreshes as specified by a payload.
///
@@ -46,5 +56,9 @@ public abstract class
public virtual void Refresh(TPayload[] payloads) =>
OnCacheUpdated(NotificationFactory.Create(payloads, MessageType.RefreshByPayload));
+ public virtual void RefreshInternal(TPayload[] payloads)
+ {
+ }
+
#endregion
}
diff --git a/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs b/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs
new file mode 100644
index 0000000000..541e9f032e
--- /dev/null
+++ b/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs
@@ -0,0 +1,121 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Cache;
+
+///
+internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
+{
+ private readonly ICoreScopeProvider _scopeProvider;
+ private readonly IRepositoryCacheVersionRepository _repositoryCacheVersionRepository;
+ private readonly ILogger _logger;
+ private readonly IRepositoryCacheVersionAccessor _repositoryCacheVersionAccessor;
+ private readonly ConcurrentDictionary _cacheVersions = new();
+
+ public RepositoryCacheVersionService(
+ ICoreScopeProvider scopeProvider,
+ IRepositoryCacheVersionRepository repositoryCacheVersionRepository,
+ ILogger logger,
+ IRepositoryCacheVersionAccessor repositoryCacheVersionAccessor)
+ {
+ _scopeProvider = scopeProvider;
+ _repositoryCacheVersionRepository = repositoryCacheVersionRepository;
+ _logger = logger;
+ _repositoryCacheVersionAccessor = repositoryCacheVersionAccessor;
+ }
+
+ ///
+ public async Task IsCacheSyncedAsync()
+ where TEntity : class
+ {
+ _logger.LogDebug("Checking if cache for {EntityType} is synced", typeof(TEntity).Name);
+
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+
+ var cacheKey = GetCacheKey();
+
+ // The cache version accessor will take a read lock if the version is not in request cache, so we don't need to take one here.
+ RepositoryCacheVersion? databaseVersion = await _repositoryCacheVersionAccessor.GetAsync(cacheKey);
+
+ if (databaseVersion?.Version is null)
+ {
+ _logger.LogDebug("Cache for {EntityType} has no version in the database, considering it synced", typeof(TEntity).Name);
+
+ // If the database version is null, it means the cache has never been initialized, so we consider it synced.
+ return true;
+ }
+
+ if (_cacheVersions.TryGetValue(cacheKey, out Guid localVersion) is false)
+ {
+ _logger.LogDebug("Cache for {EntityType} is not initialized, considering it synced", typeof(TEntity).Name);
+
+ // We're not initialized yet, so cache is empty, which means cache is synced.
+ // Since the cache is most likely no longer empty, we should set the cache version to the database version.
+ _cacheVersions[cacheKey] = Guid.Parse(databaseVersion.Version);
+ return true;
+ }
+
+ // We could've parsed this in the repository layer; however, the fact that we are using a Guid is an implementation detail.
+ if (localVersion != Guid.Parse(databaseVersion.Version))
+ {
+ _logger.LogDebug(
+ "Cache for {EntityType} is not synced: local version {LocalVersion} does not match database version {DatabaseVersion}",
+ typeof(TEntity).Name,
+ localVersion,
+ databaseVersion.Version);
+ return false;
+ }
+
+ _logger.LogDebug("Cache for {EntityType} is synced", typeof(TEntity).Name);
+ return true;
+ }
+
+ ///
+ public async Task SetCacheUpdatedAsync()
+ where TEntity : class
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+
+ // We have to take a write lock to ensure the cache is not being read while we update the version.
+ scope.WriteLock(Constants.Locks.CacheVersion);
+
+ var cacheKey = GetCacheKey();
+ var newVersion = Guid.NewGuid();
+
+ _logger.LogDebug("Setting cache for {EntityType} to version {Version}", typeof(TEntity).Name, newVersion);
+ await _repositoryCacheVersionRepository.SaveAsync(new RepositoryCacheVersion { Identifier = cacheKey, Version = newVersion.ToString() });
+ _cacheVersions[cacheKey] = newVersion;
+
+ scope.Complete();
+ }
+
+ ///
+ public async Task SetCachesSyncedAsync()
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ scope.ReadLock(Constants.Locks.CacheVersion);
+
+ // We always sync all caches versions, so it's safe to assume all caches are synced at this point.
+ IEnumerable cacheVersions = await _repositoryCacheVersionRepository.GetAllAsync();
+
+ foreach (RepositoryCacheVersion version in cacheVersions)
+ {
+ if (version.Version is null)
+ {
+ continue;
+ }
+
+ _cacheVersions[version.Identifier] = Guid.Parse(version.Version);
+ }
+
+ _repositoryCacheVersionAccessor.CachesSynced();
+ scope.Complete();
+ }
+
+ internal string GetCacheKey()
+ where TEntity : class =>
+ typeof(TEntity).FullName ?? typeof(TEntity).Name;
+}
diff --git a/src/Umbraco.Core/Cache/SingleServerCacheVersionService.cs b/src/Umbraco.Core/Cache/SingleServerCacheVersionService.cs
new file mode 100644
index 0000000000..d477ab6145
--- /dev/null
+++ b/src/Umbraco.Core/Cache/SingleServerCacheVersionService.cs
@@ -0,0 +1,23 @@
+namespace Umbraco.Cms.Core.Cache;
+
+///
+/// A simple cache version service that assumes the cache is always in sync.
+///
+/// This is useful in scenarios where you have a single server setup and do not need to manage cache synchronization across multiple servers.
+///
+///
+public class SingleServerCacheVersionService : IRepositoryCacheVersionService
+{
+ ///
+ public Task IsCacheSyncedAsync()
+ where TEntity : class
+ => Task.FromResult(true);
+
+ ///
+ public Task SetCacheUpdatedAsync()
+ where TEntity : class
+ => Task.CompletedTask;
+
+ ///
+ public Task SetCachesSyncedAsync() => Task.CompletedTask;
+}
diff --git a/src/Umbraco.Core/CacheSyncService.cs b/src/Umbraco.Core/CacheSyncService.cs
new file mode 100644
index 0000000000..c093b6f179
--- /dev/null
+++ b/src/Umbraco.Core/CacheSyncService.cs
@@ -0,0 +1,36 @@
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Factories;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Core;
+
+public class CacheSyncService : ICacheSyncService
+{
+ private readonly IMachineInfoFactory _machineInfoFactory;
+ private readonly CacheRefresherCollection _cacheRefreshers;
+ private readonly ICacheInstructionService _cacheInstructionService;
+
+ public CacheSyncService(
+ IMachineInfoFactory machineInfoFactory,
+ CacheRefresherCollection cacheRefreshers,
+ ICacheInstructionService cacheInstructionService)
+ {
+ _machineInfoFactory = machineInfoFactory;
+ _cacheRefreshers = cacheRefreshers;
+ _cacheInstructionService = cacheInstructionService;
+ }
+
+ ///
+ public void SyncAll(CancellationToken cancellationToken = default) =>
+ _cacheInstructionService.ProcessAllInstructions(
+ _cacheRefreshers,
+ cancellationToken,
+ _machineInfoFactory.GetLocalIdentity());
+
+ ///
+ public void SyncInternal(CancellationToken cancellationToken) =>
+ _cacheInstructionService.ProcessInternalInstructions(
+ _cacheRefreshers,
+ cancellationToken,
+ _machineInfoFactory.GetLocalIdentity());
+}
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index 701aad924a..28f91d4570 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -17,6 +17,7 @@ using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.DynamicRoot;
using Umbraco.Cms.Core.Editors;
using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Factories;
using Umbraco.Cms.Core.Features;
using Umbraco.Cms.Core.Handlers;
using Umbraco.Cms.Core.Hosting;
@@ -344,7 +345,11 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique(factory => new LocalizedTextService(
factory.GetRequiredService>(),
factory.GetRequiredService>()));
+ // Default to a NOOP repository cache version service
+ Services.AddUnique();
Services.AddUnique();
+ Services.AddUnique();
+ Services.AddUnique();
Services.AddUnique();
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilderExtensions.cs
new file mode 100644
index 0000000000..4d3f1df28e
--- /dev/null
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.DependencyInjection;
+
+public static partial class UmbracoBuilderExtensions
+{
+ ///
+ /// Adds the necessary components to support isolated caches in a load balanced environment.
+ ///
+ /// This is required to load balance back office.
+ public static IUmbracoBuilder LoadBalanceIsolatedCaches(this IUmbracoBuilder builder)
+ {
+ builder.Services.AddUnique();
+ return builder;
+ }
+}
diff --git a/src/Umbraco.Core/Factories/IMachineInfoFactory.cs b/src/Umbraco.Core/Factories/IMachineInfoFactory.cs
new file mode 100644
index 0000000000..a46dd7119c
--- /dev/null
+++ b/src/Umbraco.Core/Factories/IMachineInfoFactory.cs
@@ -0,0 +1,30 @@
+namespace Umbraco.Cms.Core.Factories;
+
+///
+/// Fetches information of the host machine.
+///
+public interface IMachineInfoFactory
+{
+ ///
+ /// Fetches the name of the Host Machine for identification.
+ ///
+ /// A name of the host machine.
+ public string GetMachineIdentifier();
+
+ ///
+ /// Gets the local identity for the executing AppDomain.
+ ///
+ ///
+ ///
+ /// It is not only about the "server" (machine name and appDomainappId), but also about
+ /// an AppDomain, within a Process, on that server - because two AppDomains running at the same
+ /// time on the same server (eg during a restart) are, practically, a LB setup.
+ ///
+ ///
+ /// Practically, all we really need is the guid, the other infos are here for information
+ /// and debugging purposes.
+ ///
+ ///
+ ///
+ public string GetLocalIdentity();
+}
diff --git a/src/Umbraco.Core/Factories/MachineInfoFactory.cs b/src/Umbraco.Core/Factories/MachineInfoFactory.cs
new file mode 100644
index 0000000000..f8edf0a197
--- /dev/null
+++ b/src/Umbraco.Core/Factories/MachineInfoFactory.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics;
+using Umbraco.Cms.Core.Hosting;
+
+namespace Umbraco.Cms.Core.Factories;
+
+internal sealed class MachineInfoFactory : IMachineInfoFactory
+{
+ private readonly IHostingEnvironment _hostingEnvironment;
+
+ public MachineInfoFactory(IHostingEnvironment hostingEnvironment)
+ {
+ _hostingEnvironment = hostingEnvironment;
+ }
+
+ ///
+ public string GetMachineIdentifier() => Environment.MachineName;
+
+ private string? _localIdentity;
+
+ ///
+ public string GetLocalIdentity()
+ {
+ if (_localIdentity is not null)
+ {
+ return _localIdentity;
+ }
+
+ using var process = Process.GetCurrentProcess();
+ _localIdentity = Environment.MachineName // eg DOMAIN\SERVER
+ + "/" + _hostingEnvironment.ApplicationId // eg /LM/S3SVC/11/ROOT
+ + " [P" + process.Id // eg 1234
+ + "/D" + AppDomain.CurrentDomain.Id // eg 22
+ + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique
+
+ return _localIdentity;
+ }
+}
diff --git a/src/Umbraco.Core/Models/RepositoryCacheVersion.cs b/src/Umbraco.Core/Models/RepositoryCacheVersion.cs
new file mode 100644
index 0000000000..d9dd50b35a
--- /dev/null
+++ b/src/Umbraco.Core/Models/RepositoryCacheVersion.cs
@@ -0,0 +1,17 @@
+namespace Umbraco.Cms.Core.Models;
+
+///
+/// Represents a version of a repository cache.
+///
+public class RepositoryCacheVersion
+{
+ ///
+ /// The unique identifier for the cache.
+ ///
+ public required string Identifier { get; init; }
+
+ ///
+ /// The identifier of the version of the cache.
+ ///
+ public required string? Version { get; init; }
+}
diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
index 354332413d..296ef37a84 100644
--- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
+++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
@@ -103,7 +103,12 @@ public static partial class Constants
public const string Webhook2Headers = Webhook + "2Headers";
public const string WebhookLog = Webhook + "Log";
public const string WebhookRequest = Webhook + "Request";
+
+
+ public const string RepositoryCacheVersion = TableNamePrefix + "RepositoryCacheVersion";
public const string LongRunningOperation = TableNamePrefix + "LongRunningOperation";
+
+ public const string LastSynced = TableNamePrefix + "LastSynced";
public const string DistributedJob = TableNamePrefix + "DistributedJob";
}
}
diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs
index 2a59a9c3c1..5fc9b5a6da 100644
--- a/src/Umbraco.Core/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs
@@ -91,6 +91,11 @@ public static partial class Constants
///
public const int DocumentUrls = -345;
+ ///
+ /// The cache version.
+ ///
+ public const int CacheVersion = -346;
+
///
/// All distributed jobs.
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/ILastSyncedRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ILastSyncedRepository.cs
new file mode 100644
index 0000000000..5dd9ab93bf
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/ILastSyncedRepository.cs
@@ -0,0 +1,38 @@
+namespace Umbraco.Cms.Core.Persistence.Repositories;
+
+///
+/// Handles saving and pruning of the LastSynced database table.
+///
+public interface ILastSyncedRepository
+{
+ ///
+ /// Fetches the last synced internal ID from the database.
+ ///
+ /// The Internal ID from the database.
+ Task GetInternalIdAsync();
+
+ ///
+ /// Fetches the last synced external ID from the database.
+ ///
+ /// The External ID from the database.
+ Task GetExternalIdAsync();
+
+ ///
+ /// Saves the last synced Internal ID to the Database.
+ ///
+ /// The last synced internal ID.
+ Task SaveInternalIdAsync(int id);
+
+ ///
+ /// Saves the last synced External ID to the Database.
+ ///
+ /// The last synced external ID.
+ Task SaveExternalIdAsync(int id);
+
+ ///
+ /// Deletes entries older than the set parameter. This method also removes any entries where both
+ /// IDs are higher than the lowest synced CacheInstruction ID.
+ ///
+ /// Any date entries in the DB before this parameter, will be removed from the Database.
+ Task DeleteEntriesOlderThanAsync(DateTime pruneDate);
+}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IRepositoryCacheVersionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRepositoryCacheVersionRepository.cs
new file mode 100644
index 0000000000..6fac4c834d
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IRepositoryCacheVersionRepository.cs
@@ -0,0 +1,32 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories;
+
+///
+/// Defines methods for accessing and persisting entities.
+///
+public interface IRepositoryCacheVersionRepository : IRepository
+{
+ ///
+ /// Gets a by its identifier.
+ ///
+ /// The unique identifier of the cache version.
+ ///
+ /// A if found; otherwise, null.
+ ///
+ Task GetAsync(string identifier);
+
+ ///
+ /// Gets all entities.
+ ///
+ ///
+ /// An containing all cache versions.
+ ///
+ Task> GetAllAsync();
+
+ ///
+ /// Saves the specified .
+ ///
+ /// The cache version entity to save.
+ Task SaveAsync(RepositoryCacheVersion repositoryCacheVersion);
+}
diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs
index eb13863324..b0dfa421bd 100644
--- a/src/Umbraco.Core/Services/ICacheInstructionService.cs
+++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs
@@ -1,4 +1,6 @@
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Services;
@@ -46,4 +48,40 @@ public interface ICacheInstructionService
CancellationToken cancellationToken,
string localIdentity,
int lastId);
+
+ ///
+ /// Processes all pending database cache instructions using the provided cache refreshers.
+ ///
+ /// The collection of cache refreshers to use for processing instructions.
+ /// A token to monitor for cancellation requests.
+ /// The local identity of the executing AppDomain.
+ /// The result of processing all instructions.
+ ProcessInstructionsResult ProcessAllInstructions(
+ CacheRefresherCollection cacheRefreshers,
+ CancellationToken cancellationToken,
+ string localIdentity)
+ => ProcessInstructions(
+ cacheRefreshers,
+ cancellationToken,
+ localIdentity,
+ StaticServiceProvider.Instance.GetRequiredService().GetLastSyncedExternalAsync().GetAwaiter().GetResult() ?? 0);
+
+
+ ///
+ /// Processes pending cache instructions from the database for the internal (repository) caches.
+ ///
+ /// The collection of cache refreshers to use for processing instructions.
+ /// A token to monitor for cancellation requests.
+ /// The local identity of the executing AppDomain.
+ /// The ID of the latest processed instruction.
+ /// The result of processing the internal instructions.
+ ProcessInstructionsResult ProcessInternalInstructions(
+ CacheRefresherCollection cacheRefreshers,
+ CancellationToken cancellationToken,
+ string localIdentity)
+ => ProcessInstructions(
+ cacheRefreshers,
+ cancellationToken,
+ localIdentity,
+ StaticServiceProvider.Instance.GetRequiredService().GetLastSyncedExternalAsync().GetAwaiter().GetResult() ?? 0);
}
diff --git a/src/Umbraco.Core/Sync/ILastSyncedManager.cs b/src/Umbraco.Core/Sync/ILastSyncedManager.cs
new file mode 100644
index 0000000000..4909f4e477
--- /dev/null
+++ b/src/Umbraco.Core/Sync/ILastSyncedManager.cs
@@ -0,0 +1,38 @@
+namespace Umbraco.Cms.Core.Sync;
+
+///
+/// Handles saving and pruning of the LastSynced database table.
+///
+public interface ILastSyncedManager
+{
+ ///
+ /// Fetches the last synced internal ID from the database.
+ ///
+ /// The Internal ID from the database.
+ Task GetLastSyncedInternalAsync();
+
+ ///
+ /// Fetches the last synced external ID from the database.
+ ///
+ /// The External ID from the database.
+ Task GetLastSyncedExternalAsync();
+
+ ///
+ /// Saves the last synced Internal ID to the Database.
+ ///
+ /// The last synced internal ID.
+ Task SaveLastSyncedInternalAsync(int id);
+
+ ///
+ /// Saves the last synced External ID to the Database.
+ ///
+ /// The last synced external ID.
+ Task SaveLastSyncedExternalAsync(int id);
+
+ ///
+ /// Deletes entries older than the set parameter. This method also removes any entries where both
+ /// IDs are higher than the lowest synced CacheInstruction ID.
+ ///
+ /// Any date entries in the DB before this parameter, will be removed from the Database.
+ Task DeleteOlderThanAsync(DateTime date);
+}
diff --git a/src/Umbraco.Core/Sync/LastSyncedManager.cs b/src/Umbraco.Core/Sync/LastSyncedManager.cs
new file mode 100644
index 0000000000..64b0019c6f
--- /dev/null
+++ b/src/Umbraco.Core/Sync/LastSyncedManager.cs
@@ -0,0 +1,94 @@
+using System.ComponentModel;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Sync;
+
+///
+internal sealed class LastSyncedManager : ILastSyncedManager
+{
+ private readonly ILastSyncedRepository _lastSyncedRepository;
+ private readonly ICoreScopeProvider _coreScopeProvider;
+ private int? _lastSyncedInternalId;
+ private int? _lastSyncedExternalId;
+
+ public LastSyncedManager(ILastSyncedRepository lastSyncedRepository, ICoreScopeProvider coreScopeProvider)
+ {
+ _lastSyncedRepository = lastSyncedRepository;
+ _coreScopeProvider = coreScopeProvider;
+ }
+
+ ///
+ public async Task GetLastSyncedInternalAsync()
+ {
+ if (_lastSyncedInternalId is not null)
+ {
+ return _lastSyncedInternalId;
+ }
+
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ _lastSyncedInternalId = await _lastSyncedRepository.GetInternalIdAsync();
+ scope.Complete();
+
+ return _lastSyncedInternalId;
+ }
+
+ ///
+ public async Task GetLastSyncedExternalAsync()
+ {
+ if (_lastSyncedExternalId is not null)
+ {
+ return _lastSyncedExternalId;
+ }
+
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ _lastSyncedExternalId = await _lastSyncedRepository.GetExternalIdAsync();
+ scope.Complete();
+
+ return _lastSyncedExternalId;
+ }
+
+ ///
+ public async Task SaveLastSyncedInternalAsync(int id)
+ {
+ if (id < 0)
+ {
+ throw new ArgumentException("Invalid last synced id. Must be non-negative.");
+ }
+
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ await _lastSyncedRepository.SaveInternalIdAsync(id);
+ _lastSyncedInternalId = id;
+ scope.Complete();
+ }
+
+ ///
+ public async Task SaveLastSyncedExternalAsync(int id)
+ {
+ if (id < 0)
+ {
+ throw new ArgumentException("Invalid last synced id. Must be non-negative.");
+ }
+
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ await _lastSyncedRepository.SaveExternalIdAsync(id);
+ _lastSyncedExternalId = id;
+ scope.Complete();
+ }
+
+ ///
+ public async Task DeleteOlderThanAsync(DateTime date)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ await _lastSyncedRepository.DeleteEntriesOlderThanAsync(date);
+ scope.Complete();
+ }
+
+ // Used for testing purposes only
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal void ClearLocalCache()
+ {
+ _lastSyncedInternalId = null;
+ _lastSyncedExternalId = null;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/DistributedJobs/CacheInstructionsPruningJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/DistributedJobs/CacheInstructionsPruningJob.cs
index d590dfab66..40ca6d8dea 100644
--- a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/DistributedJobs/CacheInstructionsPruningJob.cs
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/DistributedJobs/CacheInstructionsPruningJob.cs
@@ -1,7 +1,10 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
@@ -14,6 +17,7 @@ internal class CacheInstructionsPruningJob : IDistributedBackgroundJob
private readonly ICacheInstructionRepository _cacheInstructionRepository;
private readonly ICoreScopeProvider _scopeProvider;
private readonly TimeProvider _timeProvider;
+ private readonly ILastSyncedManager _lastSyncedManager;
///
/// Initializes a new instance of the class.
@@ -26,13 +30,30 @@ internal class CacheInstructionsPruningJob : IDistributedBackgroundJob
IOptions globalSettings,
ICacheInstructionRepository cacheInstructionRepository,
ICoreScopeProvider scopeProvider,
- TimeProvider timeProvider)
+ TimeProvider timeProvider,
+ ILastSyncedManager lastSyncedManager)
{
_globalSettings = globalSettings;
_cacheInstructionRepository = cacheInstructionRepository;
_scopeProvider = scopeProvider;
_timeProvider = timeProvider;
Period = globalSettings.Value.DatabaseServerMessenger.TimeBetweenPruneOperations;
+ _lastSyncedManager = lastSyncedManager;
+ }
+
+ [Obsolete("Use the constructor with ILastSyncedManager parameter instead. Scheduled for removal in Umbraco 18.")]
+ public CacheInstructionsPruningJob(
+ IOptions globalSettings,
+ ICacheInstructionRepository cacheInstructionRepository,
+ ICoreScopeProvider scopeProvider,
+ TimeProvider timeProvider)
+ : this(
+ globalSettings,
+ cacheInstructionRepository,
+ scopeProvider,
+ timeProvider,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
}
public string Name => "CacheInstructionsPruningJob";
@@ -47,6 +68,7 @@ internal class CacheInstructionsPruningJob : IDistributedBackgroundJob
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
_cacheInstructionRepository.DeleteInstructionsOlderThan(pruneDate.DateTime);
+ _lastSyncedManager.DeleteOlderThanAsync(pruneDate.DateTime).GetAwaiter().GetResult();
scope.Complete();
}
diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
index 0ac9f89b79..46de9de45e 100644
--- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs
@@ -1,6 +1,8 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
@@ -24,10 +26,29 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
private readonly RepositoryCachePolicyOptions _options;
- public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
- : base(cache, scopeAccessor) =>
+ public DefaultRepositoryCachePolicy(
+ IAppPolicyCache cache,
+ IScopeAccessor scopeAccessor,
+ RepositoryCachePolicyOptions options,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(cache, scopeAccessor, repositoryCacheVersionService, cacheSyncService) =>
_options = options ?? throw new ArgumentNullException(nameof(options));
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ public DefaultRepositoryCachePolicy(
+ IAppPolicyCache cache,
+ IScopeAccessor scopeAccessor,
+ RepositoryCachePolicyOptions options)
+ : this(
+ cache,
+ scopeAccessor,
+ options,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_";
///
@@ -98,6 +119,10 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
throw;
}
+
+ // We've changed the entity, register cache change for other servers.
+ // We assume that if something goes wrong, we'll roll back, so don't need to register the change.
+ RegisterCacheChange();
}
///
@@ -122,11 +147,16 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache.Clear(EntityTypeCacheKey);
}
+
+ // We've removed an entity, register cache change for other servers.
+ RegisterCacheChange();
}
///
public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
+
var cacheKey = GetEntityCacheKey(id);
TEntity? fromCache = Cache.GetCacheItem(cacheKey);
@@ -163,6 +193,7 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
///
public override TEntity? GetCached(TId id)
{
+ EnsureCacheIsSynced();
var cacheKey = GetEntityCacheKey(id);
return Cache.GetCacheItem(cacheKey);
}
@@ -170,6 +201,7 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
///
public override bool Exists(TId id, Func performExists, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
// if found in cache the return else check
var cacheKey = GetEntityCacheKey(id);
TEntity? fromCache = Cache.GetCacheItem(cacheKey);
@@ -179,6 +211,7 @@ public class DefaultRepositoryCachePolicy : RepositoryCachePolicyB
///
public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
if (ids?.Length > 0)
{
// try to get each entity from the cache
diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs
index f57319d122..e894d0916a 100644
--- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs
@@ -28,8 +28,8 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
private readonly Func _entityGetId;
private readonly bool _expires;
- public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires)
- : base(cache, scopeAccessor)
+ public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, IRepositoryCacheVersionService repositoryCacheVersionService, ICacheSyncService cacheSyncService, Func entityGetId, bool expires)
+ : base(cache, scopeAccessor, repositoryCacheVersionService, cacheSyncService)
{
_entityGetId = entityGetId;
_expires = expires;
@@ -100,6 +100,10 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
{
ClearAll();
}
+
+ // We've changed the entity, register cache change for other servers.
+ // We assume that if something goes wrong, we'll roll back, so don't need to register the change.
+ RegisterCacheChange();
}
///
@@ -118,11 +122,17 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
{
ClearAll();
}
+
+ // We've changed the entity, register cache change for other servers.
+ // We assume that if something goes wrong, we'll roll back, so don't need to register the change.
+ RegisterCacheChange();
}
///
public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
+
// get all from the cache, then look for the entity
IEnumerable all = GetAllCached(performGetAll);
TEntity? entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false);
@@ -135,6 +145,8 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
///
public override TEntity? GetCached(TId id)
{
+ EnsureCacheIsSynced();
+
// get all from the cache -- and only the cache, then look for the entity
DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey());
TEntity? entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false);
@@ -147,6 +159,8 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
///
public override bool Exists(TId id, Func performExits, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
+
// get all as one set, then look for the entity
IEnumerable all = GetAllCached(performGetAll);
return all.Any(x => _entityGetId(x)?.Equals(id) ?? false);
@@ -155,6 +169,8 @@ internal sealed class FullDataSetRepositoryCachePolicy : Repositor
///
public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
+
// get all as one set, from cache if possible, else repo
IEnumerable all = GetAllCached(performGetAll);
diff --git a/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs
index 807c3d2d2c..8a3dc0f050 100644
--- a/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/MemberRepositoryUsernameCachePolicy.cs
@@ -1,4 +1,7 @@
-using Umbraco.Cms.Core.Models;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
@@ -6,12 +9,39 @@ namespace Umbraco.Cms.Core.Cache;
public class MemberRepositoryUsernameCachePolicy : DefaultRepositoryCachePolicy
{
- public MemberRepositoryUsernameCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) : base(cache, scopeAccessor, options)
+ public MemberRepositoryUsernameCachePolicy(
+ IAppPolicyCache cache,
+ IScopeAccessor scopeAccessor,
+ RepositoryCachePolicyOptions options,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ cache,
+ scopeAccessor,
+ options,
+ repositoryCacheVersionService,
+ cacheSyncService)
+ {
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ public MemberRepositoryUsernameCachePolicy(
+ IAppPolicyCache cache,
+ IScopeAccessor scopeAccessor,
+ RepositoryCachePolicyOptions options)
+ : this(
+ cache,
+ scopeAccessor,
+ options,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
{
}
public IMember? GetByUserName(string key, string? username, Func performGetByUsername, Func?> performGetAll)
{
+ EnsureCacheIsSynced();
+
var cacheKey = GetEntityCacheKey(key + username);
IMember? fromCache = Cache.GetCacheItem(cacheKey);
@@ -33,6 +63,9 @@ public class MemberRepositoryUsernameCachePolicy : DefaultRepositoryCachePolicy<
public void DeleteByUserName(string key, string? username)
{
+ // We've removed an entity, register cache change for other servers.
+ RegisterCacheChange();
+
var cacheKey = GetEntityCacheKey(key + username);
Cache.ClearByKey(cacheKey);
}
diff --git a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs
index 7a43071b81..70898f12b4 100644
--- a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs
+++ b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs
@@ -1,6 +1,8 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Infrastructure.Scoping;
@@ -18,11 +20,29 @@ public abstract class RepositoryCachePolicyBase : IRepositoryCache
{
private readonly IAppPolicyCache _globalCache;
private readonly IScopeAccessor _scopeAccessor;
+ private readonly IRepositoryCacheVersionService _cacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
- protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor)
+ protected RepositoryCachePolicyBase(
+ IAppPolicyCache globalCache,
+ IScopeAccessor scopeAccessor,
+ IRepositoryCacheVersionService cacheVersionService,
+ ICacheSyncService cacheSyncService)
{
_globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache));
_scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor));
+ _cacheVersionService = cacheVersionService;
+ _cacheSyncService = cacheSyncService;
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor)
+ : this(
+ globalCache,
+ scopeAccessor,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
}
protected IAppPolicyCache Cache
@@ -68,4 +88,23 @@ public abstract class RepositoryCachePolicyBase : IRepositoryCache
///
public abstract void ClearAll();
+
+ ///
+ /// Ensures that the cache is synced with the database.
+ ///
+ protected void EnsureCacheIsSynced()
+ {
+ var synced = _cacheVersionService.IsCacheSyncedAsync().GetAwaiter().GetResult();
+ if (synced)
+ {
+ return;
+ }
+
+ _cacheSyncService.SyncInternal(CancellationToken.None);
+ }
+
+ ///
+ /// Registers a change in the cache.
+ ///
+ protected void RegisterCacheChange() => _cacheVersionService.SetCacheUpdatedAsync().GetAwaiter().GetResult();
}
diff --git a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs
index d8b0e9bb04..bae3e413d1 100644
--- a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs
+++ b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs
@@ -1,6 +1,8 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Infrastructure.Scoping;
@@ -21,8 +23,29 @@ namespace Umbraco.Cms.Core.Cache;
internal sealed class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy
where TEntity : class, IEntity
{
+ public SingleItemsOnlyRepositoryCachePolicy(
+ IAppPolicyCache cache,
+ IScopeAccessor scopeAccessor,
+ RepositoryCachePolicyOptions options,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ cache,
+ scopeAccessor,
+ options,
+ repositoryCacheVersionService,
+ cacheSyncService)
+ {
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
- : base(cache, scopeAccessor, options)
+ : this(
+ cache,
+ scopeAccessor,
+ options,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
{
}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs
index 21e715b803..00daae7c7c 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs
@@ -1,7 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Factories;
+using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Infrastructure.Sync;
using Umbraco.Extensions;
@@ -24,8 +32,19 @@ public static partial class UmbracoBuilderExtensions
{
builder.Services.AddSingleton();
builder.Services.AddSingleton();
- builder.SetServerMessenger();
-builder.AddNotificationHandler();
+ builder.SetServerMessenger(factory => new BatchedDatabaseServerMessenger(
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService>(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService(),
+ factory.GetRequiredService>(),
+ factory.GetRequiredService()));
+ builder.AddNotificationHandler();
builder.AddNotificationHandler();
return builder;
}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
index 903f01e49e..1e5d2216b9 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
@@ -82,7 +82,9 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
return builder;
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
index 828d572466..f8798816ad 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
@@ -46,6 +46,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index 5be30f86bb..711b449142 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -1079,7 +1079,7 @@ internal sealed class DatabaseDataCreator
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.LongRunningOperations, Name = "LongRunningOperations" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.DocumentUrls, Name = "DocumentUrls" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.DistributedJobs, Name = "DistributedJobs" });
-
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.CacheVersion, Name = "CacheVersion" });
}
private void CreateContentTypeData()
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index 1818e9acca..9bb389974b 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -92,6 +92,8 @@ public class DatabaseSchemaCreator
typeof(UserDataDto),
typeof(LongRunningOperationDto),
typeof(DistributedJobDto),
+ typeof(LastSyncedDto),
+ typeof(RepositoryCacheVersionDto),
};
private readonly IUmbracoDatabase _database;
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 4be78908cd..9a3f5dc8eb 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -137,5 +137,6 @@ public class UmbracoPlan : MigrationPlan
To("{1847C7FF-B021-44EB-BEB0-A77A4376A6F2}");
To("{7208B20D-6BFC-472E-9374-85EEA817B27D}");
To("{263075BF-F18A-480D-92B4-4947D2EAB772}");
+ To("26179D88-58CE-4C92-B4A4-3CBA6E7188AC");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs
index 9d2448de18..f9334ae68d 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs
@@ -76,5 +76,9 @@ public class UmbracoPremigrationPlan : MigrationPlan
// To 17.0.0
To("{D54EE168-C19D-48D8-9006-C7E719AD61FE}");
+ // The lock and table are required to access caches.
+ // When logging in, we save the user to the cache so these need to have run.
+ To("{1DC39DC7-A88A-4912-8E60-4FD36246E8D1}");
+ To("{A1B3F5D6-4C8B-4E7A-9F8C-1D2B3E4F5A6B}");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddCacheVersionDatabaseLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddCacheVersionDatabaseLock.cs
new file mode 100644
index 0000000000..7a781fd3d4
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddCacheVersionDatabaseLock.cs
@@ -0,0 +1,33 @@
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
+
+public class AddCacheVersionDatabaseLock : AsyncMigrationBase
+{
+ public AddCacheVersionDatabaseLock(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override Task MigrateAsync()
+ {
+ Sql sql = Database.SqlContext.Sql()
+ .Select()
+ .From()
+ .Where(x => x.Id == Constants.Locks.CacheVersion);
+
+ LockDto? cacheVersionLock = Database.Fetch(sql).FirstOrDefault();
+
+
+ if (cacheVersionLock is null)
+ {
+ Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.CacheVersion, Name = "CacheVersion" });
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddLastSyncedTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddLastSyncedTable.cs
new file mode 100644
index 0000000000..64634bddcb
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddLastSyncedTable.cs
@@ -0,0 +1,21 @@
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
+
+public class AddLastSyncedTable : AsyncMigrationBase
+{
+ public AddLastSyncedTable(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override Task MigrateAsync()
+ {
+ if (TableExists(LastSyncedDto.TableName) is false)
+ {
+ Create.Table().Do();
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddRepositoryCacheVersionTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddRepositoryCacheVersionTable.cs
new file mode 100644
index 0000000000..654ff3309e
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddRepositoryCacheVersionTable.cs
@@ -0,0 +1,20 @@
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
+
+public class AddRepositoryCacheVersionTable : AsyncMigrationBase
+{
+ public AddRepositoryCacheVersionTable(IMigrationContext context) : base(context)
+ {
+ }
+
+ protected override Task MigrateAsync()
+ {
+ if (TableExists(RepositoryCacheVersionDto.TableName) is false)
+ {
+ Create.Table().Do();
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LastSyncedDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LastSyncedDto.cs
new file mode 100644
index 0000000000..15c25a5ed3
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LastSyncedDto.cs
@@ -0,0 +1,29 @@
+using NPoco;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
+using Constants = Umbraco.Cms.Core.Constants;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+
+[TableName(TableName)]
+[PrimaryKey("machineId", AutoIncrement = false)]
+[ExplicitColumns]
+public class LastSyncedDto
+{
+ internal const string TableName = Constants.DatabaseSchema.Tables.LastSynced;
+
+ [Column("machineId")]
+ [PrimaryKeyColumn(Name = "PK_lastSyncedMachineId", AutoIncrement = false, Clustered = true)]
+ public required string MachineId { get; set; }
+
+ [Column("lastSyncedInternalId")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public int? LastSyncedInternalId { get; set; }
+
+ [Column("lastSyncedExternalId")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public int? LastSyncedExternalId { get; set; }
+
+ [Column("lastSyncedDate")]
+ public DateTime LastSyncedDate { get; set; }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RepositoryCacheVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RepositoryCacheVersionDto.cs
new file mode 100644
index 0000000000..a06fceed9a
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RepositoryCacheVersionDto.cs
@@ -0,0 +1,23 @@
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+[TableName(TableName)]
+[PrimaryKey("identifier", AutoIncrement = false)]
+[ExplicitColumns]
+public class RepositoryCacheVersionDto
+{
+ internal const string TableName = Constants.DatabaseSchema.Tables.RepositoryCacheVersion;
+
+ [Column("identifier")]
+ [Length(256)]
+ [PrimaryKeyColumn(Name = "PK_umbracoRepositoryCacheVersion", AutoIncrement = false, Clustered = true)]
+ public required string Identifier { get; set; }
+
+ [Column("version")]
+ [Length(256)]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public string? Version { get; set; }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs
index bb231db73b..136ae1209d 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs
@@ -27,11 +27,17 @@ internal sealed class AuditEntryRepository : EntityRepositoryBase class.
///
public AuditEntryRepository(
- IRuntimeState runtimeState,
- IScopeAccessor scopeAccessor,
+ IRuntimeState runtimeState,IScopeAccessor scopeAccessor,
AppCaches cache,
- ILogger logger)
- : base(scopeAccessor, cache, logger)
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_runtimeState = runtimeState;
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs
index 8d3cb3fb96..9b1d6a9f39 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs
@@ -15,8 +15,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class AuditRepository : EntityRepositoryBase, IAuditRepository
{
- public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger)
- : base(scopeAccessor, AppCaches.NoCache, logger)
+ public AuditRepository(
+ IScopeAccessor scopeAccessor,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ AppCaches.NoCache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs
index c211bc590d..85b02209a5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs
@@ -20,8 +20,18 @@ internal sealed class ConsentRepository : EntityRepositoryBase, I
///
/// Initializes a new instance of the class.
///
- public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public ConsentRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 32398b35f5..2cb5c0599e 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -1,9 +1,11 @@
using System.Globalization;
using System.Text.RegularExpressions;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -37,21 +39,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly IEventAggregator _eventAggregator;
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
- ///
+ protected ContentRepositoryBase(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger> logger,
+ ILanguageRepository languageRepository,
+ IRelationRepository relationRepository,
+ IRelationTypeRepository relationTypeRepository,
+ PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
+ IDataTypeService dataTypeService,
+ IEventAggregator eventAggregator,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
+ {
+ DataTypeService = dataTypeService;
+ LanguageRepository = languageRepository;
+ RelationRepository = relationRepository;
+ RelationTypeRepository = relationTypeRepository;
+ PropertyEditors = propertyEditors;
+ _dataValueReferenceFactories = dataValueReferenceFactories;
+ _eventAggregator = eventAggregator;
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
protected ContentRepositoryBase(
IScopeAccessor scopeAccessor,
AppCaches cache,
@@ -63,17 +80,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
IEventAggregator eventAggregator)
- : base(scopeAccessor, cache, logger)
+ : this(
+ scopeAccessor,
+ cache,
+ logger,
+ languageRepository,
+ relationRepository,
+ relationTypeRepository,
+ propertyEditors,
+ dataValueReferenceFactories,
+ dataTypeService,
+ eventAggregator,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
{
- DataTypeService = dataTypeService;
- LanguageRepository = languageRepository;
- RelationRepository = relationRepository;
- RelationTypeRepository = relationTypeRepository;
- PropertyEditors = propertyEditors;
- _dataValueReferenceFactories = dataValueReferenceFactories;
- _eventAggregator = eventAggregator;
}
+
protected abstract TRepository This { get; }
///
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs
index c01c0936d8..d095ddd20e 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs
@@ -21,6 +21,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
///
internal sealed class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository
{
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
+
public ContentTypeRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
@@ -28,9 +31,22 @@ internal sealed class ContentTypeRepository : ContentTypeRepositoryBase ContentType.SupportsPublishingConst;
@@ -99,7 +115,7 @@ internal sealed class ContentTypeRepository : ContentTypeRepositoryBase CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, _repositoryCacheVersionService, _cacheSyncService, GetEntityId, /*expires:*/ true);
// every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll,
// since this is a FullDataSet policy - and everything is cached
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 74f6ff4c52..35c49a5754 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -38,8 +38,15 @@ internal abstract class ContentTypeRepositoryBase : EntityRepositoryBas
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper,
- IIdKeyMap idKeyMap)
- : base(scopeAccessor, cache, logger)
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ IIdKeyMap idKeyMap,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_shortStringHelper = shortStringHelper;
CommonRepository = commonRepository;
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs
index 33d7201b31..8d821e9d2a 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs
@@ -11,8 +11,16 @@ internal sealed class DataTypeContainerRepository : EntityContainerRepository, I
public DataTypeContainerRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
- ILogger logger)
- : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DataTypeContainer)
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ Constants.ObjectTypes.DataTypeContainer,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs
index b97a200cbb..7ce707e226 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs
@@ -38,8 +38,16 @@ internal sealed class DataTypeRepository : EntityRepositoryBase,
ILogger logger,
ILoggerFactory loggerFactory,
IConfigurationEditorJsonSerializer serializer,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService,
IDataValueEditorFactory dataValueEditorFactory)
- : base(scopeAccessor, cache, logger)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService
+ )
{
_editors = editors;
_serializer = serializer;
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
index 47080a231d..79f71c33d8 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs
@@ -1,7 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -24,9 +26,15 @@ internal sealed class DictionaryRepository : EntityRepositoryBase $"{QuoteTableName(DictionaryDto.TableName)}.{QuoteColumnName(columnName)}";
- public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger,
- ILoggerFactory loggerFactory, ILanguageRepository languageRepository)
- : base(scopeAccessor, cache, logger)
+ public DictionaryRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ ILoggerFactory loggerFactory,
+ ILanguageRepository languageRepository,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(scopeAccessor, cache, logger, repositoryCacheVersionService, cacheSyncService)
{
_loggerFactory = loggerFactory;
_languageRepository = languageRepository;
@@ -34,29 +42,49 @@ internal sealed class DictionaryRepository : EntityRepositoryBase());
+ var uniqueIdRepo = new DictionaryByUniqueIdRepository(
+ this,
+ ScopeAccessor,
+ AppCaches,
+ _loggerFactory.CreateLogger(),
+ RepositoryCacheVersionService,
+ CacheSyncService);
return uniqueIdRepo.Get(uniqueId);
}
public IEnumerable GetMany(params Guid[] uniqueIds)
{
- var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches,
- _loggerFactory.CreateLogger());
+ var uniqueIdRepo = new DictionaryByUniqueIdRepository(
+ this,
+ ScopeAccessor,
+ AppCaches,
+ _loggerFactory.CreateLogger(),
+ RepositoryCacheVersionService,
+ CacheSyncService);
return uniqueIdRepo.GetMany(uniqueIds);
}
public IDictionaryItem? Get(string key)
{
- var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches,
- _loggerFactory.CreateLogger());
+ var keyRepo = new DictionaryByKeyRepository(
+ this,
+ ScopeAccessor,
+ AppCaches,
+ _loggerFactory.CreateLogger(),
+ RepositoryCacheVersionService,
+ CacheSyncService);
return keyRepo.Get(key);
}
public IEnumerable GetManyByKeys(string[] keys)
{
- var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches,
- _loggerFactory.CreateLogger());
+ var keyRepo = new DictionaryByKeyRepository(
+ this,
+ ScopeAccessor,
+ AppCaches,
+ _loggerFactory.CreateLogger(),
+ RepositoryCacheVersionService,
+ CacheSyncService);
return keyRepo.GetMany(keys);
}
@@ -127,7 +155,12 @@ internal sealed class DictionaryRepository : EntityRepositoryBase(GlobalIsolatedCache, ScopeAccessor, options);
+ return new SingleItemsOnlyRepositoryCachePolicy(
+ GlobalIsolatedCache,
+ ScopeAccessor,
+ options,
+ RepositoryCacheVersionService,
+ CacheSyncService);
}
private static IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary languagesById)
@@ -186,15 +219,29 @@ internal sealed class DictionaryRepository : EntityRepositoryBase
{
private readonly DictionaryRepository _dictionaryRepository;
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
private readonly IDictionary _languagesById;
private string QuotedColumn(string columnName) => $"{QuoteTableName(DictionaryDto.TableName)}.{QuoteColumnName(columnName)}";
- public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor,
- AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public DictionaryByUniqueIdRepository(
+ DictionaryRepository dictionaryRepository,
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_dictionaryRepository = dictionaryRepository;
+ _repositoryCacheVersionService = repositoryCacheVersionService;
+ _cacheSyncService = cacheSyncService;
_languagesById = dictionaryRepository.GetLanguagesById();
}
@@ -223,7 +270,12 @@ internal sealed class DictionaryRepository : EntityRepositoryBase(GlobalIsolatedCache, ScopeAccessor, options);
+ return new SingleItemsOnlyRepositoryCachePolicy(
+ GlobalIsolatedCache,
+ ScopeAccessor,
+ options,
+ _repositoryCacheVersionService,
+ _cacheSyncService);
}
protected override IEnumerable PerformGetAll(params Guid[]? ids)
@@ -243,15 +295,29 @@ internal sealed class DictionaryRepository : EntityRepositoryBase
{
private readonly DictionaryRepository _dictionaryRepository;
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
private readonly IDictionary _languagesById;
private string QuotedColumn(string columnName) => $"{QuoteTableName(DictionaryDto.TableName)}.{QuoteColumnName(columnName)}";
- public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor,
- AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public DictionaryByKeyRepository(
+ DictionaryRepository dictionaryRepository,
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_dictionaryRepository = dictionaryRepository;
+ _repositoryCacheVersionService = repositoryCacheVersionService;
+ _cacheSyncService = cacheSyncService;
_languagesById = dictionaryRepository.GetLanguagesById();
}
@@ -282,7 +348,12 @@ internal sealed class DictionaryRepository : EntityRepositoryBase(GlobalIsolatedCache, ScopeAccessor, options);
+ return new SingleItemsOnlyRepositoryCachePolicy(
+ GlobalIsolatedCache,
+ ScopeAccessor,
+ options,
+ _repositoryCacheVersionService,
+ _cacheSyncService);
}
protected override IEnumerable PerformGetAll(params string[]? ids)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs
index fd66e01ec0..507f583bd0 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs
@@ -11,8 +11,16 @@ internal sealed class DocumentBlueprintContainerRepository : EntityContainerRepo
public DocumentBlueprintContainerRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
- ILogger logger)
- : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentBlueprintContainer)
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ Constants.ObjectTypes.DocumentBlueprintContainer,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
index d38b9749c9..85c0aaeb2a 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -1,8 +1,10 @@
using System.Globalization;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
@@ -34,31 +36,64 @@ public class DocumentRepository : ContentRepositoryBase? _permissionRepository;
- ///
- /// Constructor
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors
- /// require services, yet these services require property editors
- ///
+ public DocumentRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches appCaches,
+ ILogger logger,
+ ILoggerFactory loggerFactory,
+ IContentTypeRepository contentTypeRepository,
+ ITemplateRepository templateRepository,
+ ITagRepository tagRepository,
+ ILanguageRepository languageRepository,
+ IRelationRepository relationRepository,
+ IRelationTypeRepository relationTypeRepository,
+ PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
+ IDataTypeService dataTypeService,
+ IJsonSerializer serializer,
+ IEventAggregator eventAggregator,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ appCaches,
+ logger,
+ languageRepository,
+ relationRepository,
+ relationTypeRepository,
+ propertyEditors,
+ dataValueReferenceFactories,
+ dataTypeService,
+ eventAggregator,
+ repositoryCacheVersionService,
+ cacheSyncService)
+ {
+ _contentTypeRepository =
+ contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository));
+ _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository));
+ _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
+ _serializer = serializer;
+ _repositoryCacheVersionService = repositoryCacheVersionService;
+ _cacheSyncService = cacheSyncService;
+ _appCaches = appCaches;
+ _loggerFactory = loggerFactory;
+ _scopeAccessor = scopeAccessor;
+ _contentByGuidReadRepository = new ContentByGuidReadRepository(
+ this,
+ scopeAccessor,
+ appCaches,
+ loggerFactory.CreateLogger(),
+ repositoryCacheVersionService,
+ cacheSyncService);
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
public DocumentRepository(
IScopeAccessor scopeAccessor,
AppCaches appCaches,
@@ -75,19 +110,25 @@ public class DocumentRepository : ContentRepositoryBase(),
+ StaticServiceProvider.Instance.GetRequiredService())
{
- _contentTypeRepository =
- contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository));
- _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository));
- _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
- _serializer = serializer;
- _appCaches = appCaches;
- _loggerFactory = loggerFactory;
- _scopeAccessor = scopeAccessor;
- _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches,
- loggerFactory.CreateLogger());
}
protected override DocumentRepository This => this;
@@ -102,7 +143,9 @@ public class DocumentRepository : ContentRepositoryBase(
_scopeAccessor,
_appCaches,
- _loggerFactory.CreateLogger>());
+ _loggerFactory.CreateLogger>(),
+ _repositoryCacheVersionService,
+ _cacheSyncService);
///
public ContentScheduleCollection GetContentSchedule(int contentId)
@@ -1560,9 +1603,19 @@ public class DocumentRepository : ContentRepositoryBase logger)
- : base(scopeAccessor, cache, logger) =>
+ public ContentByGuidReadRepository(
+ DocumentRepository outerRepo,
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService) =>
_outerRepo = outerRepo;
protected override IContent? PerformGet(Guid id)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs
index c48f7d8218..014dd47507 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs
@@ -8,8 +8,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository
{
- public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentTypeContainer)
+ public DocumentTypeContainerRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ Constants.ObjectTypes.DocumentTypeContainer,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs
index 8cceca7d00..e17270b5db 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs
@@ -15,9 +15,20 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class DomainRepository : EntityRepositoryBase, IDomainRepository
{
- public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
- { }
+ public DomainRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
+ {
+ }
public IDomain? GetByName(string domainName)
=> GetMany().FirstOrDefault(x => x.DomainName.InvariantEquals(domainName));
@@ -32,7 +43,7 @@ internal sealed class DomainRepository : EntityRepositoryBase, IDo
=> GetMany().Where(x => x.RootContentId == contentId).Where(x => includeWildcards || x.IsWildcard == false);
protected override IRepositoryCachePolicy CreateCachePolicy()
- => new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, false);
+ => new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, false);
protected override IDomain? PerformGet(int id)
// Use the underlying GetAll which will force cache all domains
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs
index 2ac35d914c..9a4927b39c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs
@@ -16,9 +16,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
///
internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository
{
- public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache,
- ILogger logger, Guid containerObjectType)
- : base(scopeAccessor, cache, logger)
+ public EntityContainerRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ Guid containerObjectType,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
Guid[] allowedContainers =
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs
index dc8abbb11f..95f0f71983 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs
@@ -1,7 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -27,9 +29,34 @@ public abstract class EntityRepositoryBase : RepositoryBase, IRead
///
/// Initializes a new instance of the class.
///
- protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger)
- : base(scopeAccessor, appCaches) =>
+ protected EntityRepositoryBase(
+ IScopeAccessor scopeAccessor,
+ AppCaches appCaches,
+ ILogger> logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(scopeAccessor, appCaches)
+ {
+ RepositoryCacheVersionService = repositoryCacheVersionService;
+ CacheSyncService = cacheSyncService;
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches,
+ ILogger> logger)
+ : this(
+ scopeAccessor,
+ appCaches,
+ logger,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ protected readonly IRepositoryCacheVersionService RepositoryCacheVersionService;
+
+ protected readonly ICacheSyncService CacheSyncService;
///
/// Gets the logger
@@ -194,7 +221,13 @@ public abstract class EntityRepositoryBase : RepositoryBase, IRead
/// Create the repository cache policy
///
protected virtual IRepositoryCachePolicy CreateCachePolicy()
- => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions);
+ => new DefaultRepositoryCachePolicy(
+ GlobalIsolatedCache,
+ ScopeAccessor,
+ DefaultOptions,
+ RepositoryCacheVersionService,
+ CacheSyncService
+ );
protected abstract TEntity? PerformGet(TId? id);
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
index 5f44f1a14d..24bc0d52cc 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs
@@ -16,11 +16,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginWithKeyRepository
{
- public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache,
- ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public ExternalLoginRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
+
///
/// Query for user tokens
///
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs
index ad1bf0a1ea..adca36788c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs
@@ -14,8 +14,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository
{
- public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger)
- : base(scopeAccessor, AppCaches.NoCache, logger)
+ public KeyValueRepository(
+ IScopeAccessor scopeAccessor,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ AppCaches.NoCache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
index 5d7467d897..f7eb89c370 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs
@@ -24,8 +24,18 @@ internal sealed class LanguageRepository : EntityRepositoryBase,
private readonly Dictionary _codeIdMap = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _idCodeMap = new();
- public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public LanguageRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
@@ -127,7 +137,7 @@ internal sealed class LanguageRepository : EntityRepositoryBase,
public int? GetDefaultId() => GetDefault().Id;
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, /*expires:*/ false);
protected ILanguage ConvertFromDto(LanguageDto dto)
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LastSyncedRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LastSyncedRepository.cs
new file mode 100644
index 0000000000..4458c04e2b
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LastSyncedRepository.cs
@@ -0,0 +1,103 @@
+using NPoco;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Factories;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
+
+///
+public class LastSyncedRepository : RepositoryBase, ILastSyncedRepository
+{
+ private readonly IMachineInfoFactory _machineInfoFactory;
+
+ public LastSyncedRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, IMachineInfoFactory machineInfoFactory)
+ : base(scopeAccessor, appCaches)
+ {
+ _machineInfoFactory = machineInfoFactory;
+ }
+
+
+ ///
+ public async Task GetInternalIdAsync()
+ {
+ string machineName = _machineInfoFactory.GetMachineIdentifier();
+
+ Sql sql = Database.SqlContext.Sql()
+ .Select(x => x.LastSyncedInternalId)
+ .From()
+ .Where(x => x.MachineId == machineName);
+
+ return await Database.ExecuteScalarAsync(sql);
+ }
+
+ ///
+ public async Task GetExternalIdAsync()
+ {
+ string machineName = _machineInfoFactory.GetMachineIdentifier();
+
+ Sql sql = Database.SqlContext.Sql()
+ .Select(x => x.LastSyncedExternalId)
+ .From()
+ .Where(x => x.MachineId == machineName);
+
+ return await Database.ExecuteScalarAsync(sql);
+ }
+
+ ///
+ public async Task SaveInternalIdAsync(int id)
+ {
+ LastSyncedDto dto = new LastSyncedDto()
+ {
+ MachineId = _machineInfoFactory.GetMachineIdentifier(),
+ LastSyncedInternalId = id,
+ LastSyncedDate = DateTime.Now,
+ };
+
+ await Database.InsertOrUpdateAsync(
+ dto,
+ "SET lastSyncedInternalId=@LastSyncedInternalId, lastSyncedDate=@LastSyncedDate WHERE machineId=@MachineId",
+ new
+ {
+ dto.LastSyncedInternalId,
+ dto.LastSyncedDate,
+ dto.MachineId,
+ });
+ }
+
+ ///
+ public async Task SaveExternalIdAsync(int id)
+ {
+ LastSyncedDto dto = new LastSyncedDto()
+ {
+ MachineId = _machineInfoFactory.GetMachineIdentifier(),
+ LastSyncedExternalId = id,
+ LastSyncedDate = DateTime.Now,
+ };
+
+ await Database.InsertOrUpdateAsync(
+ dto,
+ "SET lastSyncedExternalId=@LastSyncedExternalId, lastSyncedDate=@LastSyncedDate WHERE machineId=@MachineId",
+ new
+ {
+ dto.LastSyncedExternalId,
+ dto.LastSyncedDate,
+ dto.MachineId,
+ });
+ }
+
+ ///
+ public async Task DeleteEntriesOlderThanAsync(DateTime pruneDate)
+ {
+ var maxId = Database.ExecuteScalar($"SELECT MAX(Id) FROM umbracoCacheInstruction;");
+
+ Sql sql =
+ new Sql().Append(
+ @"DELETE FROM umbracoLastSynced WHERE lastSyncedDate < @pruneDate OR lastSyncedInternalId > @maxId AND lastSyncedExternalId > @maxId;",
+ new { pruneDate, maxId });
+
+ await Database.ExecuteAsync(sql);
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs
index 8f9011a99a..8221697347 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs
@@ -14,8 +14,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository
{
- public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public LogViewerQueryRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
@@ -25,7 +35,7 @@ internal sealed class LogViewerQueryRepository : EntityRepositoryBase x.Name == name);
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, /*expires:*/ false);
protected override IEnumerable PerformGetAll(params int[]? ids)
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
index 9ff2e3e5a9..654ea8bb54 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs
@@ -1,8 +1,10 @@
using System.Text.RegularExpressions;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -12,6 +14,7 @@ using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
@@ -48,9 +51,22 @@ public class MediaRepository : ContentRepositoryBase());
+ loggerFactory.CreateLogger(),
+ repositoryCacheVersionService,
+ cacheSyncService);
+ }
+
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ public MediaRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ ILoggerFactory loggerFactory,
+ IMediaTypeRepository mediaTypeRepository,
+ ITagRepository tagRepository,
+ ILanguageRepository languageRepository,
+ IRelationRepository relationRepository,
+ IRelationTypeRepository relationTypeRepository,
+ PropertyEditorCollection propertyEditorCollection,
+ MediaUrlGeneratorCollection mediaUrlGenerators,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
+ IDataTypeService dataTypeService,
+ IJsonSerializer serializer,
+ IEventAggregator eventAggregator)
+ : this(scopeAccessor,
+ cache,
+ logger,
+ loggerFactory,
+ mediaTypeRepository,
+ tagRepository,
+ languageRepository,
+ relationRepository,
+ relationTypeRepository,
+ propertyEditorCollection,
+ mediaUrlGenerators,
+ dataValueReferenceFactories,
+ dataTypeService,
+ serializer,
+ eventAggregator,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService()
+ )
+ {
}
protected override MediaRepository This => this;
@@ -527,8 +583,19 @@ public class MediaRepository : ContentRepositoryBase logger)
- : base(scopeAccessor, cache, logger) =>
+ public MediaByGuidReadRepository(
+ MediaRepository outerRepo,
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService) =>
_outerRepo = outerRepo;
protected override IMedia? PerformGet(Guid id)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs
index 9efd67f3aa..91a6f7616c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs
@@ -8,8 +8,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository
{
- public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger, Constants.ObjectTypes.MediaTypeContainer)
+ public MediaTypeContainerRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ Constants.ObjectTypes.MediaTypeContainer,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs
index b89c6f57e5..2d8621e02e 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs
@@ -19,6 +19,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
///
internal sealed class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository
{
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
+
public MediaTypeRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
@@ -26,9 +29,22 @@ internal sealed class MediaTypeRepository : ContentTypeRepositoryBase MediaType.SupportsPublishingConst;
@@ -36,7 +52,7 @@ internal sealed class MediaTypeRepository : ContentTypeRepositoryBase Constants.ObjectTypes.MediaType;
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, _repositoryCacheVersionService, _cacheSyncService, GetEntityId, /*expires:*/ true);
// every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll,
// since this is a FullDataSet policy - and everything is cached
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs
index 541e2606b2..857f5de365 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs
@@ -19,9 +19,19 @@ internal sealed class MemberGroupRepository : EntityRepositoryBase logger,
- IEventMessagesFactory eventMessagesFactory)
- : base(scopeAccessor, cache, logger) =>
+ public MemberGroupRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IEventMessagesFactory eventMessagesFactory,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService) =>
_eventMessagesFactory = eventMessagesFactory;
protected Guid NodeObjectTypeId => Constants.ObjectTypes.MemberGroup;
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs
index 9d7405536a..3ff8b3d97b 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -5,6 +6,7 @@ using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
@@ -56,9 +58,22 @@ public class MemberRepository : ContentRepositoryBase passwordConfiguration)
- : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository,
- propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator)
+ IOptions passwordConfiguration,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ languageRepository,
+ relationRepository,
+ relationTypeRepository,
+ propertyEditors,
+ dataValueReferenceFactories,
+ dataTypeService,
+ eventAggregator,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_memberTypeRepository =
memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository));
@@ -68,7 +83,47 @@ public class MemberRepository : ContentRepositoryBase logger,
+ IMemberTypeRepository memberTypeRepository,
+ IMemberGroupRepository memberGroupRepository,
+ ITagRepository tagRepository,
+ ILanguageRepository languageRepository,
+ IRelationRepository relationRepository,
+ IRelationTypeRepository relationTypeRepository,
+ IPasswordHasher passwordHasher,
+ PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
+ IDataTypeService dataTypeService,
+ IJsonSerializer serializer,
+ IEventAggregator eventAggregator,
+ IOptions passwordConfiguration)
+ : this(
+ scopeAccessor,
+ cache,
+ logger,
+ memberTypeRepository,
+ memberGroupRepository,
+ tagRepository,
+ languageRepository,
+ relationRepository,
+ relationTypeRepository,
+ passwordHasher,
+ propertyEditors,
+ dataValueReferenceFactories,
+ dataTypeService,
+ serializer,
+ eventAggregator,
+ passwordConfiguration,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
}
///
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
index 20b93f1259..133d529928 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
@@ -22,6 +22,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository
{
private readonly IShortStringHelper _shortStringHelper;
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
+ private readonly ICacheSyncService _cacheSyncService;
public MemberTypeRepository(
IScopeAccessor scopeAccessor,
@@ -30,16 +32,31 @@ internal sealed class MemberTypeRepository : ContentTypeRepositoryBase _shortStringHelper = shortStringHelper;
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ IIdKeyMap idKeyMap,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ commonRepository,
+ languageRepository,
+ shortStringHelper,
+ repositoryCacheVersionService,
+ idKeyMap,
+ cacheSyncService)
+ {
+ _shortStringHelper = shortStringHelper;
+ _repositoryCacheVersionService = repositoryCacheVersionService;
+ _cacheSyncService = cacheSyncService;
+ }
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;
protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MemberType;
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, _repositoryCacheVersionService, _cacheSyncService, GetEntityId, /*expires:*/ true);
// every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll,
// since this is a FullDataSet policy - and everything is cached
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs
index 5fb0c5755c..0dda5be61b 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs
@@ -26,8 +26,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class PermissionRepository : EntityRepositoryBase
where TEntity : class, IEntity
{
- public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger)
- : base(scopeAccessor, cache, logger)
+ public PermissionRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger> logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs
index a3080e6fb4..df9bf4677c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs
@@ -15,13 +15,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository
{
- public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public PublicAccessRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, /*expires:*/ false);
protected override PublicAccessEntry? PerformGet(Guid id) =>
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
index eb69ba5adf..adbe805329 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
@@ -14,8 +14,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository
{
- public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public RedirectUrlRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
index 4d9dcf834c..205f48a691 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
@@ -26,8 +26,19 @@ internal sealed class RelationRepository : EntityRepositoryBase,
private readonly IEntityRepositoryExtended _entityRepository;
private readonly IRelationTypeRepository _relationTypeRepository;
- public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository)
- : base(scopeAccessor, AppCaches.NoCache, logger)
+ public RelationRepository(
+ IScopeAccessor scopeAccessor,
+ ILogger logger,
+ IRelationTypeRepository relationTypeRepository,
+ IEntityRepositoryExtended entityRepository,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ AppCaches.NoCache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_relationTypeRepository = relationTypeRepository;
_entityRepository = entityRepository;
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs
index 2c41302cd0..71a4627278 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs
@@ -19,13 +19,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
///
internal sealed class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository
{
- public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public RelationTypeRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true);
+ new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, /*expires:*/ true);
private static void CheckNullObjectTypeValues(IRelationType entity)
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryCacheVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryCacheVersionRepository.cs
new file mode 100644
index 0000000000..334b514326
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryCacheVersionRepository.cs
@@ -0,0 +1,59 @@
+using NPoco;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
+
+///
+internal class RepositoryCacheVersionRepository : RepositoryBase, IRepositoryCacheVersionRepository
+{
+ public RepositoryCacheVersionRepository(IScopeAccessor scopeAccessor, AppCaches appCaches)
+ : base(scopeAccessor, appCaches)
+ {
+ }
+
+ ///
+ public async Task GetAsync(string identifier)
+ {
+ if (string.IsNullOrWhiteSpace(identifier))
+ {
+ throw new ArgumentException("Identifier cannot be null or whitespace.", nameof(identifier));
+ }
+
+ Sql query = Sql()
+ .Select(x => x.Version)
+ .From()
+ .Where(x => x.Identifier == identifier);
+
+ var version = await Database.ExecuteScalarAsync(query);
+
+ return new RepositoryCacheVersion { Identifier = identifier, Version = version };
+ }
+
+ public async Task> GetAllAsync()
+ {
+ Sql query = Sql()
+ .Select()
+ .From();
+
+ IEnumerable dtos = await Database.FetchAsync(query);
+ return dtos.Select(Map).Where(x => x is not null)!;
+ }
+
+ ///
+ public async Task SaveAsync(RepositoryCacheVersion repositoryCacheVersion)
+ {
+ RepositoryCacheVersionDto dto = Map(repositoryCacheVersion);
+ await Database.InsertOrUpdateAsync(dto, null, null);
+ }
+
+ private static RepositoryCacheVersionDto Map(RepositoryCacheVersion entity)
+ => new() { Identifier = entity.Identifier, Version = entity.Version };
+
+ private static RepositoryCacheVersion? Map(RepositoryCacheVersionDto? dto)
+ => dto is null ? null : new RepositoryCacheVersion { Identifier = dto.Identifier, Version = dto.Version };
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs
index 2b6c54ab98..9fb360626c 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs
@@ -14,8 +14,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class ServerRegistrationRepository : EntityRepositoryBase,
IServerRegistrationRepository
{
- public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger)
- : base(scopeAccessor, AppCaches.NoCache, logger)
+ public ServerRegistrationRepository(
+ IScopeAccessor scopeAccessor,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ AppCaches.NoCache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
@@ -44,7 +53,7 @@ internal sealed class ServerRegistrationRepository : EntityRepositoryBase(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false);
+ new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, RepositoryCacheVersionService, CacheSyncService, GetEntityId, /*expires:*/ false);
protected override int PerformCount(IQuery? query) =>
throw new NotSupportedException("This repository does not support this method.");
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs
index ad509afd5a..ebab0102fc 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs
@@ -18,8 +18,18 @@ internal abstract class SimpleGetRepository : EntityReposito
where TEntity : class, IEntity
where TDto : class
{
- protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger)
- : base(scopeAccessor, cache, logger)
+ protected SimpleGetRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger> logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
index 91d737cf53..b262e684b5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs
@@ -17,8 +17,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class TagRepository : EntityRepositoryBase, ITagRepository
{
- public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public TagRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
index eeb7f1899f..839ac87bbe 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
@@ -28,7 +28,6 @@ internal sealed class TemplateRepository : EntityRepositoryBase,
private readonly IFileSystem? _viewsFileSystem;
private readonly IViewHelper _viewHelper;
private readonly IOptionsMonitor _runtimeSettings;
-
public TemplateRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
@@ -36,8 +35,15 @@ internal sealed class TemplateRepository : EntityRepositoryBase,
FileSystems fileSystems,
IShortStringHelper shortStringHelper,
IViewHelper viewHelper,
- IOptionsMonitor runtimeSettings)
- : base(scopeAccessor, cache, logger)
+ IOptionsMonitor runtimeSettings,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_shortStringHelper = shortStringHelper;
_viewsFileSystem = fileSystems.MvcViewsFileSystem;
@@ -85,8 +91,13 @@ internal sealed class TemplateRepository : EntityRepositoryBase,
}
protected override IRepositoryCachePolicy CreateCachePolicy() =>
- new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor,
- GetEntityId, /*expires:*/ false);
+ new FullDataSetRepositoryCachePolicy(
+ GlobalIsolatedCache,
+ ScopeAccessor,
+ RepositoryCacheVersionService,
+ CacheSyncService,
+ GetEntityId,
+ /*expires:*/ false);
private IEnumerable GetAxisDefinitions(params TemplateDto[] templates)
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs
index 1cc77f090c..3f72b081ef 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs
@@ -14,9 +14,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
internal sealed class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository
{
- public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache,
- ILogger logger)
- : base(scopeAccessor, cache, logger)
+ public TwoFactorLoginRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs
index 99d2c7cd7a..97d2e60a40 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs
@@ -1,8 +1,10 @@
using System.Collections;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
@@ -35,15 +37,53 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG
ILogger logger,
ILoggerFactory loggerFactory,
IShortStringHelper shortStringHelper,
- IEnumerable permissionMappers)
- : base(scopeAccessor, appCaches, logger)
+ IEnumerable permissionMappers,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ appCaches,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_shortStringHelper = shortStringHelper;
- _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger());
- _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>());
+ _userGroupWithUsersRepository = new UserGroupWithUsersRepository(
+ this,
+ scopeAccessor,
+ appCaches,
+ loggerFactory.CreateLogger(),
+ repositoryCacheVersionService,
+ cacheSyncService);
+ _permissionRepository = new PermissionRepository(
+ scopeAccessor,
+ appCaches,
+ loggerFactory.CreateLogger>(),
+ repositoryCacheVersionService,
+ cacheSyncService);
_permissionMappers = permissionMappers.ToDictionary(x => x.Context);
}
+ [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
+ public UserGroupRepository(
+ IScopeAccessor scopeAccessor,
+ AppCaches appCaches,
+ ILogger logger,
+ ILoggerFactory loggerFactory,
+ IShortStringHelper shortStringHelper,
+ IEnumerable permissionMappers)
+ : this(
+ scopeAccessor,
+ appCaches,
+ logger,
+ loggerFactory,
+ shortStringHelper,
+ permissionMappers,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
public IUserGroup? Get(string alias)
{
try
@@ -198,8 +238,19 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG
{
private readonly UserGroupRepository _userGroupRepo;
- public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger)
- : base(scopeAccessor, cache, logger) =>
+ public UserGroupWithUsersRepository(
+ UserGroupRepository userGroupRepo,
+ IScopeAccessor scopeAccessor,
+ AppCaches cache,
+ ILogger logger,
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ cache,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService) =>
_userGroupRepo = userGroupRepo;
protected override void PersistNewItem(UserGroupWithUsers entity)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
index 6246cd4598..3e20c95ed8 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
@@ -54,6 +54,8 @@ internal sealed class UserRepository : EntityRepositoryBase, IUserR
/// The JSON serializer.
/// State of the runtime.
/// The permission mappers.
+ /// The app policy cache.
+ ///
///
/// mapperCollection
/// or
@@ -70,8 +72,15 @@ internal sealed class UserRepository : EntityRepositoryBase, IUserR
IOptions passwordConfiguration,
IJsonSerializer jsonSerializer,
IRuntimeState runtimeState,
- IEnumerable permissionMappers)
- : base(scopeAccessor, appCaches, logger)
+ IRepositoryCacheVersionService repositoryCacheVersionService,
+ IEnumerable permissionMappers,
+ ICacheSyncService cacheSyncService)
+ : base(
+ scopeAccessor,
+ appCaches,
+ logger,
+ repositoryCacheVersionService,
+ cacheSyncService)
{
_mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection));
_globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings));
diff --git a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs
index 29499e3bcb..5395bb5142 100644
--- a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs
+++ b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs
@@ -1,9 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
@@ -26,11 +28,12 @@ namespace Umbraco.Cms
private readonly ICacheInstructionRepository _cacheInstructionRepository;
private readonly GlobalSettings _globalSettings;
private readonly ILogger _logger;
+ private readonly ILastSyncedManager _lastSyncedManager;
+ private readonly IRepositoryCacheVersionService _repositoryCacheVersionService;
private readonly IProfilingLogger _profilingLogger;
+ private readonly Lock _syncLock = new();
- ///
- /// Initializes a new instance of the class.
- ///
+ [Obsolete("Use the overload that requires ILastSyncedManager and IRepositoryCacheVersionService. Scheduled for removal in V18.")]
public CacheInstructionService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
@@ -39,11 +42,36 @@ namespace Umbraco.Cms
IProfilingLogger profilingLogger,
ILogger logger,
IOptions globalSettings)
+ : this(
+ provider,
+ loggerFactory,
+ eventMessagesFactory,
+ cacheInstructionRepository,
+ profilingLogger,
+ logger,
+ globalSettings,
+ StaticServiceProvider.Instance.GetRequiredService