From d9f8faf50909963ec6269025f9376cc8160e0060 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 8 Oct 2025 14:55:50 +0200 Subject: [PATCH] Load balancing: Load balance isolated caches to allow the backoffice to be load balanced (#20417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * V16: Cache Version Mechanism (#19747) * Add RepositoryCacheVersion table * Add repository * Add Cache version lock * Add GetAll method to repository * Add RepositoryCacheVersionService * Remember to add lock in data creator * Work my way out of constructor hell This is why we use DI folks. 🤦 * Add checks to specific cache policies * Fix migration * Add to schema creator * Fix database access * Initialize the cache version on in memory miss * Make cache version service internal * Add tests * Apply suggestions from code review Co-authored-by: Andy Butland * Add missing obsoletions * Prefer full name --------- Co-authored-by: Andy Butland * fixed merge * V16/feature/move last synced id to db (#19884) * Foundation work for moving last synced id * register manager and repo in dependency injection * Fixing to make tests work * Replacing the use of the old LastSyncedFileManager.cs with the new LastSyncedManager.cs * Testing to delete out of sync id and old entries * changing some stuff to please the reviewer. * Inverted saving methods id check and fixed documentation mishaps * Loadbalancing: Add Cache Sync service to allow us to roll forward isolated caches when backoffice is load balanced. (#20398) * Split cache refreshers into internal and external caches * Add obsolete constructor for CacheInstructionsPruningJob * Add xml docs * Move lastID management into CacheInstructionService * Cache last synced ids in memory * Lock when processing instructions * Sync caches when out of sync * Fix constructors for ICacheSyncService * Cache version on request * Register caches as synced when instructions are processed * Rename CacheVersionAccessor to IRepositoryCacheVersionAccessor * Set caches as synced before actually syncing the caches * Set caches as synced before syncing, within scope, this should also lock the cache version from being written to whilst updating caches * Only check version for backoffice requests * Clear request cache when caches are syned * Default to using NOOP cache version service * Don't generate local identity in database server messenger anymore * Fix ambiguous constructor * Add helper method to switch to load balanced isolated caches * Fix LastSyncedManagerTests * Fix RepositoryCacheVersionServiceTests * Fix DefaultCachePolicyTests * Use correct constructor in FullDataSetRepositoryCachePolicy * Minor cleanup * Add XML docs * Add more xml docs * Apply suggestions from code review Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --------- Co-authored-by: Zeegaan Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Fix migration plan * fix tests * Fix integration tests * Fix changes from github review * Move premigrations to v17 * Make lock constantws sequential * Fix comment * Make IRepositoryCacheVersionService and ICacheSyncService protected on EntityRepositoryBase --------- Co-authored-by: Andy Butland Co-authored-by: Nicklas Kramer Co-authored-by: NillasKA Co-authored-by: Zeegaan Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- src/Umbraco.Core/Cache/ICacheSyncService.cs | 33 +++++ .../Cache/IRepositoryCacheVersionAccessor.cs | 33 +++++ .../Cache/IRepositoryCacheVersionService.cs | 28 ++++ .../Cache/Refreshers/IJsonCacheRefresher.cs | 6 + .../Refreshers/IPayloadCacheRefresher.cs | 6 + .../Implement/ContentCacheRefresher.cs | 14 +- .../Implement/ContentTypeCacheRefresher.cs | 7 +- .../Implement/DataTypeCacheRefresher.cs | 12 +- .../Implement/DomainCacheRefresher.cs | 6 +- .../Implement/LanguageCacheRefresher.cs | 16 +-- .../Implement/MediaCacheRefresher.cs | 48 ++++--- .../Implement/MemberCacheRefresher.cs | 4 +- .../Implement/MemberGroupCacheRefresher.cs | 4 +- .../Implement/UserCacheRefresher.cs | 4 +- .../Implement/ValueEditorCacheRefresher.cs | 3 +- .../Refreshers/JsonCacheRefresherBase.cs | 5 + .../Refreshers/PayloadCacheRefresherBase.cs | 14 ++ .../Cache/RepositoryCacheVersionService.cs | 121 ++++++++++++++++ .../Cache/SingleServerCacheVersionService.cs | 23 ++++ src/Umbraco.Core/CacheSyncService.cs | 36 +++++ .../DependencyInjection/UmbracoBuilder.cs | 5 + .../UmbracoBuilderExtensions.cs | 17 +++ .../Factories/IMachineInfoFactory.cs | 30 ++++ .../Factories/MachineInfoFactory.cs | 37 +++++ .../Models/RepositoryCacheVersion.cs | 17 +++ .../Persistence/Constants-DatabaseSchema.cs | 5 + .../Persistence/Constants-Locks.cs | 5 + .../Repositories/ILastSyncedRepository.cs | 38 ++++++ .../IRepositoryCacheVersionRepository.cs | 32 +++++ .../Services/ICacheInstructionService.cs | 38 ++++++ src/Umbraco.Core/Sync/ILastSyncedManager.cs | 38 ++++++ src/Umbraco.Core/Sync/LastSyncedManager.cs | 94 +++++++++++++ .../CacheInstructionsPruningJob.cs | 24 +++- .../Cache/DefaultRepositoryCachePolicy.cs | 37 ++++- .../Cache/FullDataSetRepositoryCachePolicy.cs | 20 ++- .../MemberRepositoryUsernameCachePolicy.cs | 37 ++++- .../Cache/RepositoryCachePolicyBase.cs | 41 +++++- .../SingleItemsOnlyRepositoryCachePolicy.cs | 25 +++- .../UmbracoBuilder.DistributedCache.cs | 23 +++- .../UmbracoBuilder.Repositories.cs | 2 + .../UmbracoBuilder.Services.cs | 1 + .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Install/DatabaseSchemaCreator.cs | 2 + .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/UmbracoPremigrationPlan.cs | 4 + .../V_17_0_0/AddCacheVersionDatabaseLock.cs | 33 +++++ .../Upgrade/V_17_0_0/AddLastSyncedTable.cs | 21 +++ .../AddRepositoryCacheVersionTable.cs | 20 +++ .../Persistence/Dtos/LastSyncedDto.cs | 29 ++++ .../Dtos/RepositoryCacheVersionDto.cs | 23 ++++ .../Implement/AuditEntryRepository.cs | 14 +- .../Repositories/Implement/AuditRepository.cs | 13 +- .../Implement/ConsentRepository.cs | 14 +- .../Implement/ContentRepositoryBase.cs | 69 ++++++---- .../Implement/ContentTypeRepository.cs | 22 ++- .../Implement/ContentTypeRepositoryBase.cs | 11 +- .../Implement/DataTypeContainerRepository.cs | 12 +- .../Implement/DataTypeRepository.cs | 10 +- .../Implement/DictionaryRepository.cs | 111 ++++++++++++--- .../DocumentBlueprintContainerRepository.cs | 12 +- .../Implement/DocumentRepository.cs | 127 ++++++++++++----- .../DocumentTypeContainerRepository.cs | 15 +- .../Implement/DomainRepository.cs | 19 ++- .../Implement/EntityContainerRepository.cs | 16 ++- .../Implement/EntityRepositoryBase.cs | 39 +++++- .../Implement/ExternalLoginRepository.cs | 16 ++- .../Implement/KeyValueRepository.cs | 13 +- .../Implement/LanguageRepository.cs | 16 ++- .../Implement/LastSyncedRepository.cs | 103 ++++++++++++++ .../Implement/LogViewerQueryRepository.cs | 16 ++- .../Repositories/Implement/MediaRepository.cs | 79 ++++++++++- .../Implement/MediaTypeContainerRepository.cs | 15 +- .../Implement/MediaTypeRepository.cs | 22 ++- .../Implement/MemberGroupRepository.cs | 16 ++- .../Implement/MemberRepository.cs | 63 ++++++++- .../Implement/MemberTypeRepository.cs | 25 +++- .../Implement/PermissionRepository.cs | 14 +- .../Implement/PublicAccessRepository.cs | 16 ++- .../Implement/RedirectUrlRepository.cs | 14 +- .../Implement/RelationRepository.cs | 15 +- .../Implement/RelationTypeRepository.cs | 16 ++- .../RepositoryCacheVersionRepository.cs | 59 ++++++++ .../Implement/ServerRegistrationRepository.cs | 15 +- .../Implement/SimpleGetRepository.cs | 14 +- .../Repositories/Implement/TagRepository.cs | 14 +- .../Implement/TemplateRepository.cs | 21 ++- .../Implement/TwoFactorLoginRepository.cs | 15 +- .../Implement/UserGroupRepository.cs | 63 ++++++++- .../Repositories/Implement/UserRepository.cs | 13 +- .../Services/CacheInstructionService.cs | 90 +++++++++++- .../Sync/BatchedDatabaseServerMessenger.cs | 45 ++++-- .../Sync/DatabaseServerMessenger.cs | 99 ++++++++------ .../Sync/LastSyncedFileManager.cs | 1 + .../Sync/ServerMessengerBase.cs | 2 + .../Cache/RepositoryCacheVersionAccessor.cs | 81 +++++++++++ .../UmbracoBuilderExtensions.cs | 2 + .../Testing/UmbracoIntegrationTest.cs | 3 +- .../Cache/DistributedCacheRefresherTests.cs | 2 + .../Request/ApiContentRequestTestBase.cs | 10 +- .../Request/ApiContentResponseBuilderTests.cs | 11 +- .../RepositoryCacheVersionServiceTests.cs | 108 +++++++++++++++ .../Repositories/AuditRepositoryTest.cs | 20 +-- .../Repositories/ContentTypeRepositoryTest.cs | 4 +- .../Repositories/DictionaryRepositoryTest.cs | 4 +- .../Repositories/DocumentRepositoryTest.cs | 14 +- .../Repositories/DomainRepositoryTest.cs | 3 +- .../Repositories/KeyValueRepositoryTests.cs | 4 +- .../Repositories/LanguageRepositoryTest.cs | 3 +- .../Repositories/MediaRepositoryTest.cs | 15 +- .../Repositories/MediaTypeRepositoryTest.cs | 5 +- .../Repositories/MemberTypeRepositoryTest.cs | 2 +- .../PublicAccessRepositoryTest.cs | 16 ++- .../RedirectUrlRepositoryTests.cs | 4 +- .../Repositories/RelationRepositoryTest.cs | 4 +- .../RelationTypeRepositoryTest.cs | 5 +- .../ServerRegistrationRepositoryTest.cs | 3 +- .../Repositories/TagRepositoryTest.cs | 3 +- .../Repositories/TemplateRepositoryTest.cs | 12 +- .../Repositories/UserRepositoryTest.cs | 8 +- .../Services/RedirectUrlServiceTests.cs | 8 +- .../Sync/LastSyncedManagerTest.cs | 129 ++++++++++++++++++ .../Cache/DefaultCachePolicyTests.cs | 12 +- .../Cache/FullDataSetCachePolicyTests.cs | 16 +-- .../Cache/SingleItemsOnlyCachePolicyTests.cs | 4 +- .../Jobs/CacheInstructionsPruningJobTests.cs | 3 +- 125 files changed, 2694 insertions(+), 362 deletions(-) create mode 100644 src/Umbraco.Core/Cache/ICacheSyncService.cs create mode 100644 src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs create mode 100644 src/Umbraco.Core/Cache/IRepositoryCacheVersionService.cs create mode 100644 src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs create mode 100644 src/Umbraco.Core/Cache/SingleServerCacheVersionService.cs create mode 100644 src/Umbraco.Core/CacheSyncService.cs create mode 100644 src/Umbraco.Core/DependencyInjection/UmbracoBuilderExtensions.cs create mode 100644 src/Umbraco.Core/Factories/IMachineInfoFactory.cs create mode 100644 src/Umbraco.Core/Factories/MachineInfoFactory.cs create mode 100644 src/Umbraco.Core/Models/RepositoryCacheVersion.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ILastSyncedRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IRepositoryCacheVersionRepository.cs create mode 100644 src/Umbraco.Core/Sync/ILastSyncedManager.cs create mode 100644 src/Umbraco.Core/Sync/LastSyncedManager.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddCacheVersionDatabaseLock.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddLastSyncedTable.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/AddRepositoryCacheVersionTable.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/LastSyncedDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/RepositoryCacheVersionDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LastSyncedRepository.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryCacheVersionRepository.cs create mode 100644 src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/RepositoryCacheVersionServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/LastSyncedManagerTest.cs 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(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public CacheInstructionService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + ICacheInstructionRepository cacheInstructionRepository, + IProfilingLogger profilingLogger, + ILogger logger, + IOptions globalSettings, + ILastSyncedManager lastSyncedManager, + IRepositoryCacheVersionService repositoryCacheVersionService) : base(provider, loggerFactory, eventMessagesFactory) { _cacheInstructionRepository = cacheInstructionRepository; _profilingLogger = profilingLogger; _logger = logger; + _lastSyncedManager = lastSyncedManager; + _repositoryCacheVersionService = repositoryCacheVersionService; _globalSettings = globalSettings.Value; } @@ -119,7 +147,7 @@ namespace Umbraco.Cms } } - /// + [Obsolete("Use non obsolete version instead, scheduled for removal in V18.")] public ProcessInstructionsResult ProcessInstructions( CacheRefresherCollection cacheRefreshers, CancellationToken cancellationToken, @@ -135,6 +163,59 @@ namespace Umbraco.Cms } } + /// + public ProcessInstructionsResult ProcessAllInstructions( + CacheRefresherCollection cacheRefreshers, + CancellationToken cancellationToken, + string localIdentity) + { + lock (_syncLock) + { + using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration("Syncing from database...")) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _repositoryCacheVersionService.SetCachesSyncedAsync(); + var lastId = _lastSyncedManager.GetLastSyncedExternalAsync().GetAwaiter().GetResult() ?? 0; + var numberOfInstructionsProcessed = ProcessDatabaseInstructions(cacheRefreshers, cancellationToken, localIdentity, ref lastId); + + if (numberOfInstructionsProcessed > 0) + { + _lastSyncedManager.SaveLastSyncedExternalAsync(lastId).GetAwaiter().GetResult(); + _lastSyncedManager.SaveLastSyncedInternalAsync(lastId).GetAwaiter().GetResult(); + } + + scope.Complete(); + return ProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); + } + } + } + + /// + public ProcessInstructionsResult ProcessInternalInstructions( + CacheRefresherCollection cacheRefreshers, + CancellationToken cancellationToken, + string localIdentity) + { + lock (_syncLock) + { + using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration("Syncing from database...")) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _repositoryCacheVersionService.SetCachesSyncedAsync(); + var lastId = _lastSyncedManager.GetLastSyncedInternalAsync().GetAwaiter().GetResult() ?? 0; + var numberOfInstructionsProcessed = ProcessDatabaseInstructions(cacheRefreshers, cancellationToken, localIdentity, ref lastId); + + if (numberOfInstructionsProcessed > 0) + { + _lastSyncedManager.SaveLastSyncedInternalAsync(lastId).GetAwaiter().GetResult(); + } + + scope.Complete(); + return ProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); + } + } + } + private CacheInstruction CreateCacheInstruction(IEnumerable instructions, string localIdentity) => new( 0, @@ -428,6 +509,7 @@ namespace Umbraco.Cms IJsonCacheRefresher refresher = GetJsonRefresher(cacheRefreshers, uniqueIdentifier); if (jsonPayload is not null) { + refresher.RefreshInternal(jsonPayload); refresher.Refresh(jsonPayload); } } diff --git a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs index a6380df4b4..2cf6e31b55 100644 --- a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs @@ -1,7 +1,10 @@ +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.Runtime; using Umbraco.Cms.Core.Serialization; @@ -18,9 +21,35 @@ public class BatchedDatabaseServerMessenger : DatabaseServerMessenger { private readonly IRequestCache _requestCache; - /// - /// Initializes a new instance of the class. - /// + public BatchedDatabaseServerMessenger( + IMainDom mainDom, + CacheRefresherCollection cacheRefreshers, + ILogger logger, + ISyncBootStateAccessor syncBootStateAccessor, + IHostingEnvironment hostingEnvironment, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, + IRequestCache requestCache, + ILastSyncedManager lastSyncedManager, + IOptionsMonitor globalSettings, + IMachineInfoFactory machineInfoFactory) + : base( + mainDom, + cacheRefreshers, + logger, + true, + syncBootStateAccessor, + hostingEnvironment, + cacheInstructionService, + jsonSerializer, + globalSettings, + lastSyncedManager, + machineInfoFactory) + { + _requestCache = requestCache; + } + + [Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V18.")] public BatchedDatabaseServerMessenger( IMainDom mainDom, CacheRefresherCollection cacheRefreshers, @@ -32,19 +61,19 @@ public class BatchedDatabaseServerMessenger : DatabaseServerMessenger IRequestCache requestCache, LastSyncedFileManager lastSyncedFileManager, IOptionsMonitor globalSettings) - : base( + : this( mainDom, cacheRefreshers, logger, - true, syncBootStateAccessor, hostingEnvironment, cacheInstructionService, jsonSerializer, - lastSyncedFileManager, - globalSettings) + requestCache, + StaticServiceProvider.Instance.GetRequiredService(), + globalSettings, + StaticServiceProvider.Instance.GetRequiredService()) { - _requestCache = requestCache; } /// diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index 096318d349..05fa6ee47e 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -1,8 +1,11 @@ using System.Diagnostics; +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.Runtime; using Umbraco.Cms.Core.Serialization; @@ -21,7 +24,8 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IHostingEnvironment _hostingEnvironment; private readonly Lazy _initialized; - private readonly LastSyncedFileManager _lastSyncedFileManager; + private readonly ILastSyncedManager _lastSyncedManager; + private readonly IMachineInfoFactory _machineInfoFactory; private readonly Lock _locko = new(); /* @@ -40,6 +44,7 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable /// /// Initializes a new instance of the class. /// + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V18.")] protected DatabaseServerMessenger( IMainDom mainDom, CacheRefresherCollection cacheRefreshers, @@ -51,33 +56,20 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable IJsonSerializer jsonSerializer, LastSyncedFileManager lastSyncedFileManager, IOptionsMonitor globalSettings) - : base(distributedEnabled, jsonSerializer) + : this( + mainDom, + cacheRefreshers, + logger, + distributedEnabled, + syncBootStateAccessor, + hostingEnvironment, + cacheInstructionService, + jsonSerializer, + globalSettings, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) { - _cancellationToken = _cancellationTokenSource.Token; - _mainDom = mainDom; - _cacheRefreshers = cacheRefreshers; - _hostingEnvironment = hostingEnvironment; - Logger = logger; - _syncBootStateAccessor = syncBootStateAccessor; - CacheInstructionService = cacheInstructionService; - JsonSerializer = jsonSerializer; - _lastSyncedFileManager = lastSyncedFileManager; - GlobalSettings = globalSettings.CurrentValue; - _lastSync = DateTime.UtcNow; - _syncIdle = new ManualResetEvent(true); - - globalSettings.OnChange(x => GlobalSettings = x); - using (var process = Process.GetCurrentProcess()) - { - // See notes on _localIdentity - 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 - } - - _initialized = new Lazy(InitializeWithMainDom); } /// @@ -110,6 +102,42 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable { } + /// + /// Initializes a new instance of the class. + /// + protected DatabaseServerMessenger( + IMainDom mainDom, + CacheRefresherCollection cacheRefreshers, + ILogger logger, + bool distributedEnabled, + ISyncBootStateAccessor syncBootStateAccessor, + IHostingEnvironment hostingEnvironment, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, + IOptionsMonitor globalSettings, + ILastSyncedManager lastSyncedManager, + IMachineInfoFactory machineInfoFactory) + : base(distributedEnabled, jsonSerializer) + { + _cancellationToken = _cancellationTokenSource.Token; + _mainDom = mainDom; + _cacheRefreshers = cacheRefreshers; + _hostingEnvironment = hostingEnvironment; + Logger = logger; + _syncBootStateAccessor = syncBootStateAccessor; + CacheInstructionService = cacheInstructionService; + JsonSerializer = jsonSerializer; + GlobalSettings = globalSettings.CurrentValue; + _lastSync = DateTime.UtcNow; + _syncIdle = new ManualResetEvent(true); + _lastSyncedManager = lastSyncedManager; + _machineInfoFactory = machineInfoFactory; + + globalSettings.OnChange(x => GlobalSettings = x); + + _initialized = new Lazy(InitializeWithMainDom); + } + public GlobalSettings GlobalSettings { get; private set; } protected ILogger Logger { get; } @@ -132,7 +160,7 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable /// and debugging purposes. /// /// - protected string LocalIdentity { get; } + protected string LocalIdentity => _machineInfoFactory.GetLocalIdentity(); /// /// Synchronize the server (throttled). @@ -170,16 +198,10 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable try { - ProcessInstructionsResult result = CacheInstructionService.ProcessInstructions( + CacheInstructionService.ProcessAllInstructions( _cacheRefreshers, _cancellationToken, - LocalIdentity, - _lastSyncedFileManager.LastSyncedId); - - if (result.LastId > 0) - { - _lastSyncedFileManager.SaveLastSyncedId(result.LastId); - } + LocalIdentity); } finally { @@ -297,15 +319,16 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable if (syncState == SyncBootState.ColdBoot) { + var lastSyncedId = _lastSyncedManager.GetLastSyncedExternalAsync().GetAwaiter().GetResult() ?? -1; // Get the last id in the db and store it. // Note: Do it BEFORE initializing otherwise some instructions might get lost // when doing it before. Some instructions might run twice but this is not an issue. var maxId = CacheInstructionService.GetMaxInstructionId(); // if there is a max currently, or if we've never synced - if (maxId > 0 || _lastSyncedFileManager.LastSyncedId < 0) + if (maxId > 0 || lastSyncedId < 0) { - _lastSyncedFileManager.SaveLastSyncedId(maxId); + _lastSyncedManager.SaveLastSyncedExternalAsync(maxId).GetAwaiter().GetResult(); } } diff --git a/src/Umbraco.Infrastructure/Sync/LastSyncedFileManager.cs b/src/Umbraco.Infrastructure/Sync/LastSyncedFileManager.cs index c569809329..7e42f431ca 100644 --- a/src/Umbraco.Infrastructure/Sync/LastSyncedFileManager.cs +++ b/src/Umbraco.Infrastructure/Sync/LastSyncedFileManager.cs @@ -4,6 +4,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Sync; +[Obsolete("Use the LastSyncedManager class instead. Scheduled for removal in V18")] public sealed class LastSyncedFileManager { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs b/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs index 35ffbc4985..fdfb692389 100644 --- a/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Infrastructure/Sync/ServerMessengerBase.cs @@ -206,6 +206,7 @@ public abstract class ServerMessengerBase : IServerMessenger throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IPayloadCacheRefresher)); } + payloadRefresher.RefreshInternal(payload); payloadRefresher.Refresh(payload); } @@ -265,6 +266,7 @@ public abstract class ServerMessengerBase : IServerMessenger if (json is not null) { + jsonRefresher.RefreshInternal(json); jsonRefresher.Refresh(json); } diff --git a/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs b/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs new file mode 100644 index 0000000000..b96e2546e1 --- /dev/null +++ b/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Cache; + +public class RepositoryCacheVersionAccessor : IRepositoryCacheVersionAccessor +{ + private readonly IRequestCache _requestCache; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IRepositoryCacheVersionRepository _repositoryCacheVersionRepository; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly ILogger _logger; + + public RepositoryCacheVersionAccessor( + IRequestCache requestCache, + IHttpContextAccessor httpContextAccessor, + IRepositoryCacheVersionRepository repositoryCacheVersionRepository, + ICoreScopeProvider coreScopeProvider, + ILogger logger) + { + _requestCache = requestCache; + _httpContextAccessor = httpContextAccessor; + _repositoryCacheVersionRepository = repositoryCacheVersionRepository; + _coreScopeProvider = coreScopeProvider; + _logger = logger; + } + + /// + /// 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. + /// + /// + /// This method implements a two-tier caching strategy: + /// + /// First checks the request cache to avoid database queries within the same request. + /// If not found in request cache, queries the database and caches the result for subsequent calls. + /// + /// Client-side requests always return + /// to avoid unnecessary cache version lookups. + /// + public async Task GetAsync(string cacheKey) + { + HttpContext? httpcontext = _httpContextAccessor.HttpContext; + if (httpcontext?.RequestServices is not null && httpcontext.Request.IsBackOfficeRequest() is false) + { + _logger.LogDebug("Client side request detected, skipping cache version retrieval for key {CacheKey}", cacheKey); + // We don't want to try and fetch version for client side requests, always assume we're in sync. + return null; + } + + RepositoryCacheVersion? requestCachedVersion = _requestCache.GetCacheItem(cacheKey); + if (requestCachedVersion is not null) + { + _logger.LogDebug("Cache version for key {CacheKey} found in request cache", cacheKey); + return requestCachedVersion; + } + + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + scope.ReadLock(Core.Constants.Locks.CacheVersion); + + RepositoryCacheVersion? databaseVersion = await _repositoryCacheVersionRepository.GetAsync(cacheKey); + + if (databaseVersion is null) + { + return databaseVersion; + } + + _requestCache.Set(cacheKey, databaseVersion); + return databaseVersion; + } + + public void CachesSynced() => _requestCache.ClearOfType(); +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 18629d7cfd..b3d31d323b 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -44,6 +44,7 @@ using Umbraco.Cms.Web.Common; using Umbraco.Cms.Web.Common.ApplicationModels; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Blocks; +using Umbraco.Cms.Web.Common.Cache; using Umbraco.Cms.Web.Common.Configuration; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -102,6 +103,7 @@ public static partial class UmbracoBuilderExtensions // is just based on AsyncLocal, see https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/HttpContextAccessor.cs IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); services.AddSingleton(httpContextAccessor); + services.AddUnique(); var requestCache = new HttpContextRequestAppCache(httpContextAccessor); var appCaches = AppCaches.Create(requestCache); diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index f880a3c068..ee48bc78b9 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -22,7 +22,7 @@ using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.Attributes; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; - +using Umbraco.Cms.Web.Common.Cache; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Testing; @@ -187,6 +187,7 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase // custom helper services that might be moved out of tests eventually to benefit the community services.AddSingleton(); + services.AddUnique(); builder.Build(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs index 0b9fd4755d..a1e2afbf82 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/DistributedCacheRefresherTests.cs @@ -25,6 +25,7 @@ internal sealed class DistributedCacheRefresherTests : UmbracoIntegrationTest var cacheKey = "test"; PopulateCache("test"); + ContentCacheRefresher.RefreshInternal([new ContentCacheRefresher.JsonPayload()]); ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload()]); Assert.IsNull(ElementsCache.Get(cacheKey)); @@ -36,6 +37,7 @@ internal sealed class DistributedCacheRefresherTests : UmbracoIntegrationTest var cacheKey = "test"; PopulateCache("test"); + MediaCacheRefresher.RefreshInternal([new MediaCacheRefresher.JsonPayload(1, Guid.NewGuid(), TreeChangeTypes.RefreshAll)]); MediaCacheRefresher.Refresh([new MediaCacheRefresher.JsonPayload(1, Guid.NewGuid(), TreeChangeTypes.RefreshAll)]); Assert.IsNull(ElementsCache.Get(cacheKey)); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs index 8ec8e92bba..2db35dde58 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentRequestTestBase.cs @@ -89,5 +89,13 @@ public abstract class ApiContentRequestTestBase : UmbracoIntegrationTest } protected void RefreshContentCache() - => Services.GetRequiredService().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); + { + var refresher = Services.GetRequiredService(); + ContentCacheRefresher.JsonPayload[] payloads = + [ + new() { ChangeTypes = TreeChangeTypes.RefreshAll } + ]; + refresher.RefreshInternal(payloads); + refresher.Refresh(payloads); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentResponseBuilderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentResponseBuilderTests.cs index 02319df2a1..11dab200ca 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentResponseBuilderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/Request/ApiContentResponseBuilderTests.cs @@ -99,5 +99,14 @@ public class ApiContentResponseBuilderTests : UmbracoIntegrationTest } private void RefreshContentCache() - => GetRequiredService().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); + { + var refresher = GetRequiredService(); + ContentCacheRefresher.JsonPayload[] payloads = + [ + new() { ChangeTypes = TreeChangeTypes.RefreshAll } + ]; + + refresher.RefreshInternal(payloads); + refresher.Refresh(payloads); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/RepositoryCacheVersionServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/RepositoryCacheVersionServiceTests.cs new file mode 100644 index 0000000000..564758232c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/RepositoryCacheVersionServiceTests.cs @@ -0,0 +1,108 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] +internal sealed class RepositoryCacheVersionServiceTests : UmbracoIntegrationTest +{ + private IRepositoryCacheVersionService RepositoryCacheVersionService => GetRequiredService(); + + private IRepositoryCacheVersionRepository RepositoryCacheVersionRepository => GetRequiredService(); + + private ICoreScopeProvider CoreScopeProvider => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.LoadBalanceIsolatedCaches(); + + [Test] + public async Task Cache_Is_Initially_Synced() + { + var isSynced = await RepositoryCacheVersionService.IsCacheSyncedAsync(); + Assert.IsTrue(isSynced, "Cache should be initially synced."); + } + + [Test] + public async Task SetCacheUpdatedAsync_Writes_Version_To_Database() + { + using var scope = CoreScopeProvider.CreateCoreScope(autoComplete: true); + var initial = await RepositoryCacheVersionRepository.GetAsync(GetCacheKey()); + Assert.IsNull(initial?.Version, "Initial cache version should be null before update."); + + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + + var cacheVersion = await RepositoryCacheVersionRepository.GetAsync(GetCacheKey()); + Assert.IsNotNull(cacheVersion, "Cache version should exist in the database after update."); + Assert.IsFalse(string.IsNullOrEmpty(cacheVersion.Version), "Cache version string should not be null or empty."); + } + + [Test] + public async Task Cache_Is_Out_Of_Sync_If_Updated_Remotely() + { + // Simulate an update + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + var isSynced = await RepositoryCacheVersionService.IsCacheSyncedAsync(); + + // We should be synced now. + Assert.IsTrue(isSynced); + + using (var scope = CoreScopeProvider.CreateCoreScope()) + { + // Simulate a remote update to the database + await RepositoryCacheVersionRepository.SaveAsync(GetRepositoryRandomCacheVersion()); + scope.Complete(); + } + + // Now the cache should be out of sync + isSynced = await RepositoryCacheVersionService.IsCacheSyncedAsync(); + Assert.IsFalse(isSynced, "Cache should be out of sync after remote update."); + } + + [Test] + public async Task SetCacheUpdatedAsync_Updates_Cache_Version() + { + using var scope = CoreScopeProvider.CreateCoreScope(autoComplete: true); + + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + var initialCacheVersion = await RepositoryCacheVersionRepository.GetAsync(GetCacheKey()); + Assert.IsNotNull(initialCacheVersion, "Initial cache version should not be null."); + + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + var updatedCacheVersion = await RepositoryCacheVersionRepository.GetAsync(GetCacheKey()); + Assert.IsNotNull(updatedCacheVersion, "Updated cache version should not be null."); + + Assert.AreNotEqual(initialCacheVersion.Version, updatedCacheVersion.Version, "Cache version should be updated."); + } + + [Test] + public async Task CacheVersion_Is_Unique_Per_Repository_Type() + { + using var scope = CoreScopeProvider.CreateCoreScope(autoComplete: true); + + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + await RepositoryCacheVersionService.SetCacheUpdatedAsync(); + + var contentVersion = (await RepositoryCacheVersionRepository.GetAsync(GetCacheKey()))?.Version; + var mediaKey = ((RepositoryCacheVersionService)RepositoryCacheVersionService).GetCacheKey(); + var mediaVersion = (await RepositoryCacheVersionRepository.GetAsync(mediaKey))?.Version; + + Assert.IsNotNull(contentVersion); + Assert.IsNotNull(mediaVersion); + Assert.AreNotEqual(contentVersion, mediaVersion, "Cache versions should be unique for different repository types."); + } + + private RepositoryCacheVersion GetRepositoryRandomCacheVersion() + => new() + { + Identifier = GetCacheKey(), + Version = Guid.NewGuid().ToString(), + }; + + private string GetCacheKey() + => ((RepositoryCacheVersionService)RepositoryCacheVersionService).GetCacheKey(); +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs index e206abbd44..ad714361a3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/AuditRepositoryTest.cs @@ -2,8 +2,10 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence; @@ -34,7 +36,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest var sp = ScopeProvider; using (var scope = ScopeProvider.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); repo.Save(new AuditItem(-1, AuditType.System, -1, UmbracoObjectTypes.Document.GetName(), "This is a System audit trail")); var dtos = ScopeAccessor.AmbientScope.Database.Fetch("WHERE id > -1"); @@ -82,7 +84,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); for (var i = 0; i < 100; i++) { @@ -95,7 +97,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); var page = repo.GetPagedResultsByQuery(sp.CreateQuery(), 0, 10, out var total, Direction.Descending, null, null); @@ -110,7 +112,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); for (var i = 0; i < 100; i++) { @@ -123,7 +125,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); var query = sp.CreateQuery().Where(x => x.UserId == -1); @@ -159,7 +161,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); for (var i = 0; i < 100; i++) { @@ -172,7 +174,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); var page = repo.GetPagedResultsByQuery( sp.CreateQuery(), @@ -196,7 +198,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); for (var i = 0; i < 100; i++) { @@ -209,7 +211,7 @@ internal sealed class AuditRepositoryTest : UmbracoIntegrationTest using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor)sp, _logger); + var repo = new AuditRepository((IScopeAccessor)sp, _logger, Mock.Of(), Mock.Of()); var page = repo.GetPagedResultsByQuery( sp.CreateQuery(), diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index 24a066d965..0dc81e3468 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -92,7 +92,9 @@ internal sealed class ContentTypeRepositoryTest : UmbracoIntegrationTest FileSystems, ShortStringHelper, Mock.Of(), - runtimeSettingsMock.Object); + runtimeSettingsMock.Object, + Mock.Of(), + Mock.Of()); var repository = ContentTypeRepository; Template[] templates = { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index acdda16406..086be5c15b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -33,7 +33,9 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest cache, GetRequiredService>(), GetRequiredService(), - GetRequiredService()); + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); [Test] public async Task Can_Perform_Get_By_Key_On_DictionaryRepository() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index a396a7628a..80c6a7d930 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -111,6 +111,8 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest LoggerFactory.CreateLogger(), LoggerFactory, ConfigurationEditorJsonSerializer, + Mock.Of(), + Mock.Of(), Services.GetRequiredService()); return ctRepository; } @@ -125,16 +127,16 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest var runtimeSettingsMock = new Mock>(); runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings()); - templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, ShortStringHelper, Mock.Of(), runtimeSettingsMock.Object); - var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); + templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, ShortStringHelper, Mock.Of(), runtimeSettingsMock.Object, Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper); var languageRepository = - new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap); - var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); + new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, Mock.Of(), IdKeyMap, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); - var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); + var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository, Mock.Of(), Mock.Of()); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs index e0885097af..c8e0e9fed8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DomainRepositoryTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; @@ -32,7 +33,7 @@ internal sealed class DomainRepositoryTest : UmbracoIntegrationTest { var accessor = (IScopeAccessor)provider; var domainRepository = - new DomainRepository(accessor, AppCaches.NoCache, LoggerFactory.CreateLogger()); + new DomainRepository(accessor, AppCaches.NoCache, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); return domainRepository; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs index 1a1740d11e..89fd355a32 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/KeyValueRepositoryTests.cs @@ -2,7 +2,9 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -64,5 +66,5 @@ internal sealed class KeyValueRepositoryTests : UmbracoIntegrationTest } private IKeyValueRepository CreateRepository(ICoreScopeProvider provider) => - new KeyValueRepository((IScopeAccessor)provider, LoggerFactory.CreateLogger()); + new KeyValueRepository((IScopeAccessor)provider, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs index 676a74fe40..f25f8bd3e6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/LanguageRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -365,7 +366,7 @@ internal sealed class LanguageRepositoryTest : UmbracoIntegrationTest } } - private LanguageRepository CreateRepository(IScopeProvider provider) => new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger()); + private LanguageRepository CreateRepository(IScopeProvider provider) => new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); private async Task CreateTestData() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index c0f1bb54ec..f0cf94851c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -51,16 +51,15 @@ internal sealed class MediaRepositoryTest : UmbracoIntegrationTest { appCaches ??= AppCaches.NoCache; var scopeAccessor = (IScopeAccessor)provider; - var globalSettings = new GlobalSettings(); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, TemplateRepository, appCaches, ShortStringHelper); var languageRepository = - new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); - mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap); - var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); - var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); + new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); + mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, Mock.Of(), IdKeyMap, Mock.Of()); + var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); - var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); + var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository, Mock.Of(), Mock.Of()); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var mediaUrlGenerators = new MediaUrlGeneratorCollection(() => Enumerable.Empty()); @@ -81,7 +80,9 @@ internal sealed class MediaRepositoryTest : UmbracoIntegrationTest dataValueReferences, DataTypeService, JsonSerializer, - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of()); return repository; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs index a05d5b96c9..f5551d751a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -412,8 +413,8 @@ internal sealed class MediaTypeRepositoryTest : UmbracoIntegrationTest } private MediaTypeRepository CreateRepository(IScopeProvider provider) => - new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), CommonRepository, LanguageRepository, ShortStringHelper, IdKeyMap); + new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), CommonRepository, LanguageRepository, ShortStringHelper, Mock.Of(), IdKeyMap, Mock.Of()); private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) => - new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Constants.ObjectTypes.MediaTypeContainer); + new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Constants.ObjectTypes.MediaTypeContainer, Mock.Of(), Mock.Of()); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs index 6392354a6f..d0becbde25 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -27,7 +27,7 @@ internal sealed class MemberTypeRepositoryTest : UmbracoIntegrationTest { var commonRepository = GetRequiredService(); var languageRepository = GetRequiredService(); - return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap); + return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of>(), commonRepository, languageRepository, ShortStringHelper, Mock.Of(), IdKeyMap, Mock.Of()); } [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs index 75d18639d3..9fcbf9c598 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence; @@ -33,7 +35,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); PublicAccessRule[] rules = { new PublicAccessRule { RuleValue = "test", RuleType = "RoleName" } }; var entry = new PublicAccessEntry(content[0], content[1], content[2], rules); @@ -55,7 +57,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest using (var scope = provider.CreateScope()) { ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); PublicAccessRule[] rules = { new PublicAccessRule { RuleValue = "test", RuleType = "RoleName" } }; var entry = new PublicAccessEntry(content[0], content[1], content[2], rules); @@ -89,7 +91,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest using (var scope = provider.CreateScope()) { ScopeAccessor.AmbientScope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); PublicAccessRule[] rules = { @@ -123,7 +125,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); PublicAccessRule[] rules = { new PublicAccessRule { RuleValue = "test", RuleType = "RoleName" } }; var entry = new PublicAccessEntry(content[0], content[1], content[2], rules); @@ -152,7 +154,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(),Mock.Of(), Mock.Of()); PublicAccessRule[] rules = { new PublicAccessRule { RuleValue = "test", RuleType = "RoleName" } }; var entry = new PublicAccessEntry(content[0], content[1], content[2], rules); @@ -173,7 +175,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var allEntries = new List(); for (var i = 0; i < 10; i++) @@ -233,7 +235,7 @@ internal sealed class PublicAccessRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { - var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + var repo = new PublicAccessRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); PublicAccessRule[] rules1 = { new PublicAccessRule { RuleValue = "test", RuleType = "RoleName" } }; var entry1 = new PublicAccessEntry(content[0], content[1], content[2], rules1); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs index f1cf2548a7..a45c1438a9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RedirectUrlRepositoryTests.cs @@ -3,7 +3,9 @@ using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; @@ -235,7 +237,7 @@ internal sealed class RedirectUrlRepositoryTests : UmbracoIntegrationTest } private IRedirectUrlRepository CreateRepository(IScopeProvider provider) => - new RedirectUrlRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger()); + new RedirectUrlRepository((IScopeAccessor)provider, AppCaches, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); private IContent _textpage; private IContent _subpage; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs index 002c7a463b..85aa7b0fc8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs @@ -574,9 +574,9 @@ internal sealed class RelationRepositoryTest : UmbracoIntegrationTest using (var scope = ScopeProvider.CreateScope()) { var accessor = (IScopeAccessor)ScopeProvider; - var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Mock.Of>()); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Mock.Of>(), Mock.Of(), Mock.Of()); var entityRepository = new EntityRepository(accessor, AppCaches.Disabled); - var relationRepository = new RelationRepository(accessor, Mock.Of>(), relationTypeRepository, entityRepository); + var relationRepository = new RelationRepository(accessor, Mock.Of>(), relationTypeRepository, entityRepository, Mock.Of(), Mock.Of()); relationTypeRepository.Save(_relateContent); relationTypeRepository.Save(_relateContentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs index 948cd07287..becaf5898c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -23,7 +24,7 @@ internal sealed class RelationTypeRepositoryTest : UmbracoIntegrationTest public void SetUp() => CreateTestData(); private RelationTypeRepository CreateRepository(ICoreScopeProvider provider) => - new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger()); + new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); [Test] public void Can_Perform_Add_On_RelationTypeRepository() @@ -240,7 +241,7 @@ internal sealed class RelationTypeRepositoryTest : UmbracoIntegrationTest ICoreScopeProvider provider = ScopeProvider; using (var scope = provider.CreateCoreScope()) { - var repository = new RelationTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger()); + var repository = new RelationTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); repository.Save(relateContent); // Id 2 repository.Save(relateContentType); // Id 3 diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index 4f089252e9..395a0f6703 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Data.Common; using System.Linq; using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -28,7 +29,7 @@ internal sealed class ServerRegistrationRepositoryTest : UmbracoIntegrationTest private AppCaches _appCaches; private ServerRegistrationRepository CreateRepository(IScopeProvider provider) => - new((IScopeAccessor)provider, LoggerFactory.CreateLogger()); + new((IScopeAccessor)provider, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); [Test] public void Cannot_Add_Duplicate_Server_Identities() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs index d70fc9c191..a6ae0e334e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TagRepositoryTest.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -1140,5 +1141,5 @@ internal sealed class TagRepositoryTest : UmbracoIntegrationTest } private TagRepository CreateRepository(IScopeProvider provider) => - new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger()); + new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 13c4db5c3f..97cc3bfa79 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -63,7 +63,7 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest private IOptionsMonitor RuntimeSettings => GetRequiredService>(); private ITemplateRepository CreateRepository(IScopeProvider provider) => - new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, ShortStringHelper, ViewHelper, RuntimeSettings); + new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, ShortStringHelper, ViewHelper, RuntimeSettings, Mock.Of(), Mock.Of()); [Test] public void Can_Instantiate_Repository() @@ -262,14 +262,14 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest var templateRepository = CreateRepository(provider); var globalSettings = new GlobalSettings(); var serializer = new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()); - var tagRepository = new TagRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); + var tagRepository = new TagRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches, ShortStringHelper); - var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); - var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap); - var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); + var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); + var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper, Mock.Of(), IdKeyMap, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Mock.Of()); var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); - var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); + var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository, Mock.Of(), Mock.Of()); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs index 4b4012d1bf..a0e91bdd87 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs @@ -57,7 +57,9 @@ internal sealed class UserRepositoryTest : UmbracoIntegrationTest Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()), mockRuntimeState.Object, - PermissionMappers); + Mock.Of(), + PermissionMappers, + Mock.Of()); return repository; } @@ -164,7 +166,9 @@ internal sealed class UserRepositoryTest : UmbracoIntegrationTest Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()), mockRuntimeState.Object, - PermissionMappers); + Mock.Of(), + PermissionMappers, + Mock.Of()); repository2.Delete(user); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs index 4fa34ee982..2e6bb099f2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RedirectUrlServiceTests.cs @@ -38,8 +38,12 @@ internal sealed class RedirectUrlServiceTests : UmbracoIntegrationTestWithConten using (var scope = ScopeProvider.CreateScope()) { - var repository = new RedirectUrlRepository((IScopeAccessor)ScopeProvider, AppCaches.Disabled, - Mock.Of>()); + var repository = new RedirectUrlRepository( + (IScopeAccessor)ScopeProvider, + AppCaches.Disabled, + Mock.Of>(), + Mock.Of(), + Mock.Of()); var rootContent = ContentService.GetRootContent().First(); var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList(); _firstSubPage = subPages[0]; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/LastSyncedManagerTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/LastSyncedManagerTest.cs new file mode 100644 index 0000000000..092319496b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/LastSyncedManagerTest.cs @@ -0,0 +1,129 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Sync; + + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class LastSyncedManagerTest : UmbracoIntegrationTest +{ + private LastSyncedManager manager => (LastSyncedManager)GetRequiredService(); + + [Test] + public async Task Last_Synced_Internal_Id_Is_Initially_Null() + { + var value = await manager.GetLastSyncedInternalAsync(); + Assert.IsNull(value); + } + + [Test] + public async Task Last_Synced_External_Id_Is_Initially_Null() + { + var value = await manager.GetLastSyncedExternalAsync(); + Assert.IsNull(value); + } + + [Test] + public async Task Last_Synced_Internal_Id_Cannot_Be_Negative() + { + Assert.Throws(() => manager.SaveLastSyncedInternalAsync(-1).GetAwaiter().GetResult()); + } + + [Test] + public async Task Last_Synced_External_Id_Cannot_Be_Negative() + { + Assert.Throws(() => manager.SaveLastSyncedExternalAsync(-1).GetAwaiter().GetResult()); + } + + [Test] + public async Task Save_Last_Synced_Internal_Id() + { + Random random = new Random(); + int testId = random.Next(); + await manager.SaveLastSyncedInternalAsync(testId); + int? lastSynced = await manager.GetLastSyncedInternalAsync(); + + Assert.AreEqual(testId, lastSynced); + } + + [Test] + public async Task Save_Last_Synced_External_Id() + { + Random random = new Random(); + int testId = random.Next(); + await manager.SaveLastSyncedExternalAsync(testId); + int? lastSynced = await manager.GetLastSyncedExternalAsync(); + + Assert.AreEqual(testId, lastSynced); + } + + [Test] + public async Task Delete_Old_Synced_External_Id() + { + Random random = new Random(); + int testId = random.Next(); + await manager.SaveLastSyncedExternalAsync(testId); + manager.ClearLocalCache(); + + // Make sure not to delete if not too old. + await manager.DeleteOlderThanAsync(DateTime.Now - TimeSpan.FromDays(1)); + int? lastSynced = await manager.GetLastSyncedExternalAsync(); + Assert.NotNull(lastSynced); + manager.ClearLocalCache(); + + // Make sure to delete if too old. + await manager.DeleteOlderThanAsync(DateTime.Now + TimeSpan.FromDays(1)); + lastSynced = await manager.GetLastSyncedExternalAsync(); + Assert.Null(lastSynced); + } + + [Test] + public async Task Delete_Old_Synced_Internal_Id() + { + Random random = new Random(); + int testId = random.Next(); + await manager.SaveLastSyncedInternalAsync(testId); + manager.ClearLocalCache(); + + // Make sure not to delete if not too old. + await manager.DeleteOlderThanAsync(DateTime.Now - TimeSpan.FromDays(1)); + int? lastSynced = await manager.GetLastSyncedInternalAsync(); + Assert.NotNull(lastSynced); + manager.ClearLocalCache(); + + // Make sure to delete if too old. + await manager.DeleteOlderThanAsync(DateTime.Now + TimeSpan.FromDays(1)); + lastSynced = await manager.GetLastSyncedInternalAsync(); + Assert.Null(lastSynced); + } + + [Test] + public async Task Delete_Out_Of_Sync_Id() + { + using (ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)ScopeProvider); + repo.Add(new CacheInstruction(0, DateTime.Now, "{}", "Test", 1)); + + Assert.IsTrue(repo.Exists(1)); + + await manager.SaveLastSyncedExternalAsync(2); + await manager.SaveLastSyncedInternalAsync(2); + manager.ClearLocalCache(); + + + Assert.NotNull(await manager.GetLastSyncedExternalAsync()); + manager.ClearLocalCache(); + + await manager.DeleteOlderThanAsync(DateTime.Now); + + Assert.Null(await manager.GetLastSyncedExternalAsync()); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs index fc156e431d..429b6b392a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/DefaultCachePolicyTests.cs @@ -36,7 +36,7 @@ public class DefaultCachePolicyTests .Callback(() => isCached = true); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); var unused = defaultPolicy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), o => null); Assert.IsTrue(isCached); @@ -49,7 +49,7 @@ public class DefaultCachePolicyTests cache.Setup(x => x.Get(It.IsAny())).Returns(new AuditItem(1, AuditType.Copy, 123, "test", "blah")); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); var found = defaultPolicy.Get(1, id => null, ids => null); Assert.IsNotNull(found); @@ -65,7 +65,7 @@ public class DefaultCachePolicyTests cache.Setup(x => x.SearchByKey(It.IsAny())).Returns(new AuditItem[] { }); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); var unused = defaultPolicy.GetAll( new object[] { }, @@ -89,7 +89,7 @@ public class DefaultCachePolicyTests }); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); var found = defaultPolicy.GetAll(new object[] { }, ids => new[] { (AuditItem)null }); Assert.AreEqual(2, found.Length); @@ -104,7 +104,7 @@ public class DefaultCachePolicyTests .Callback(() => cacheCleared = true); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); try { defaultPolicy.Update(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); @@ -128,7 +128,7 @@ public class DefaultCachePolicyTests .Callback(() => cacheCleared = true); var defaultPolicy = - new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + new DefaultRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), new SingleServerCacheVersionService(), Mock.Of()); try { defaultPolicy.Delete(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs index 6547be90df..c73c8749d5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/FullDataSetCachePolicyTests.cs @@ -45,7 +45,7 @@ public class FullDataSetCachePolicyTests .Callback(() => isCached = true); var policy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); var unused = policy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), ids => getAll); Assert.IsTrue(isCached); @@ -63,7 +63,7 @@ public class FullDataSetCachePolicyTests cache.Setup(x => x.Get(It.IsAny())).Returns(new AuditItem(1, AuditType.Copy, 123, "test", "blah")); var defaultPolicy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); var found = defaultPolicy.Get(1, id => null, ids => getAll); Assert.IsNotNull(found); @@ -92,7 +92,7 @@ public class FullDataSetCachePolicyTests .Returns(() => cached.Any() ? new DeepCloneableList(ListCloneBehavior.CloneOnce) : null); var policy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); var found = policy.GetAll(new object[] { }, ids => getAll); @@ -100,7 +100,7 @@ public class FullDataSetCachePolicyTests Assert.IsNotNull(list); // Do it again, ensure that its coming from the cache! - policy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + policy = new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); found = policy.GetAll(new object[] { }, ids => getAll); @@ -131,7 +131,7 @@ public class FullDataSetCachePolicyTests cache.Setup(x => x.Get(It.IsAny())).Returns(new AuditItem[] { }); var defaultPolicy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); var found = defaultPolicy.GetAll(new object[] { }, ids => getAll); @@ -154,7 +154,7 @@ public class FullDataSetCachePolicyTests }); var defaultPolicy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); var found = defaultPolicy.GetAll(new object[] { }, ids => getAll); Assert.AreEqual(2, found.Length); @@ -175,7 +175,7 @@ public class FullDataSetCachePolicyTests .Callback(() => cacheCleared = true); var defaultPolicy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); try { defaultPolicy.Update(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); @@ -205,7 +205,7 @@ public class FullDataSetCachePolicyTests .Callback(() => cacheCleared = true); var defaultPolicy = - new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, item => item.Id, false); + new FullDataSetRepositoryCachePolicy(cache.Object, DefaultAccessor, new SingleServerCacheVersionService(), Mock.Of(), item => item.Id, false); try { defaultPolicy.Delete(new AuditItem(1, AuditType.Copy, 123, "test", "blah"), item => throw new Exception("blah!")); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs index 67c86a3af8..a5ae7d3e85 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/SingleItemsOnlyCachePolicyTests.cs @@ -36,7 +36,7 @@ public class SingleItemsOnlyCachePolicyTests .Callback((string cacheKey, Func o, TimeSpan? t, bool b) => cached.Add(cacheKey)); cache.Setup(x => x.SearchByKey(It.IsAny())).Returns(new AuditItem[] { }); - var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), Mock.Of(), Mock.Of()); var unused = defaultPolicy.GetAll( new object[] { }, @@ -57,7 +57,7 @@ public class SingleItemsOnlyCachePolicyTests cache.Setup(x => x.Insert(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) .Callback(() => isCached = true); - var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions()); + var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, DefaultAccessor, new RepositoryCachePolicyOptions(), Mock.Of(), Mock.Of()); var unused = defaultPolicy.Get(1, id => new AuditItem(1, AuditType.Copy, 123, "test", "blah"), ids => null); Assert.IsTrue(isCached); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs index 45432489c1..314d1ddaaf 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs; @@ -69,7 +70,7 @@ public class CacheInstructionsPruningJobTests .Setup(g => g.Value) .Returns(globalSettings); - return new CacheInstructionsPruningJob(_globalSettingsMock.Object, _cacheInstructionRepositoryMock.Object, _scopeProviderMock.Object, _timeProviderMock.Object); + return new CacheInstructionsPruningJob(_globalSettingsMock.Object, _cacheInstructionRepositoryMock.Object, _scopeProviderMock.Object, _timeProviderMock.Object, Mock.Of()); } private void SetupScopeProviderMock() =>