From fed8157d4de3747afedbb65859d989d6375d9df9 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Mon, 15 Dec 2025 10:18:41 +0000 Subject: [PATCH] Add cleanup in DocumentUrl tables when the content key changes. --- .../Documents/ISyncDocumentUrlCleaner.cs | 7 ++++ .../Documents/SyncDocumentUrlCleaner.cs | 36 +++++++++++++++++++ .../Serializers/ContentSerializer.cs | 16 +++++++-- .../Serializers/ContentSerializerBase.cs | 11 ++++++ .../Serializers/ContentTemplateSerializer.cs | 6 ++-- uSync.Core/uSyncCoreBuilderExtensions.cs | 4 +++ 6 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 uSync.Core/Documents/ISyncDocumentUrlCleaner.cs create mode 100644 uSync.Core/Documents/SyncDocumentUrlCleaner.cs diff --git a/uSync.Core/Documents/ISyncDocumentUrlCleaner.cs b/uSync.Core/Documents/ISyncDocumentUrlCleaner.cs new file mode 100644 index 000000000..06bee455a --- /dev/null +++ b/uSync.Core/Documents/ISyncDocumentUrlCleaner.cs @@ -0,0 +1,7 @@ + +namespace uSync.Core.Documents; + +public interface ISyncDocumentUrlCleaner +{ + void CleanUrlsForDocument(Guid key); +} \ No newline at end of file diff --git a/uSync.Core/Documents/SyncDocumentUrlCleaner.cs b/uSync.Core/Documents/SyncDocumentUrlCleaner.cs new file mode 100644 index 000000000..85036bcd5 --- /dev/null +++ b/uSync.Core/Documents/SyncDocumentUrlCleaner.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace uSync.Core.Documents; + +internal class SyncDocumentUrlCleaner : ISyncDocumentUrlCleaner +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDocumentUrlRepository _documentUrlRepository; + private readonly ILogger _logger; + + public SyncDocumentUrlCleaner(IDocumentUrlRepository documentUrlRepository, ILogger logger, ICoreScopeProvider scopeProvider) + { + _documentUrlRepository = documentUrlRepository; + _logger = logger; + _scopeProvider = scopeProvider; + } + + public void CleanUrlsForDocument(Guid key) + { + try + { + using (_scopeProvider.CreateCoreScope(autoComplete: true)) + { + _logger.LogDebug("Cleaning urls for document {DocumentKey}", key); + _documentUrlRepository.DeleteByDocumentKey([key]); + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Error cleaning urls for document {DocumentKey}", key); + } + } +} diff --git a/uSync.Core/Serialization/Serializers/ContentSerializer.cs b/uSync.Core/Serialization/Serializers/ContentSerializer.cs index 4b159a14a..533602383 100644 --- a/uSync.Core/Serialization/Serializers/ContentSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentSerializer.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; +using uSync.Core.Documents; using uSync.Core.Extensions; using uSync.Core.Mapping; using uSync.Core.Models; @@ -23,6 +24,7 @@ public class ContentSerializer : ContentSerializerBase, ISyncSerialize protected readonly IUserService userService; protected readonly ITemplateService _templateService; + protected readonly ISyncDocumentUrlCleaner _urlCleaner; public ContentSerializer( IEntityService entityService, @@ -33,7 +35,8 @@ public ContentSerializer( IContentService contentService, SyncValueMapperCollection syncMappers, IUserService userService, - ITemplateService templateService) + ITemplateService templateService, + ISyncDocumentUrlCleaner urlCleaner) : base(entityService, languageService, relationService, shortStringHelper, logger, UmbracoObjectTypes.Document, syncMappers) { this.contentService = contentService; @@ -41,6 +44,7 @@ public ContentSerializer( this.relationAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; this.userService = userService; _templateService = templateService; + _urlCleaner = urlCleaner; } #region Serialization @@ -298,7 +302,7 @@ public override async Task> DeserializeSecondPassAsync(ICo var changes = await DeserializeSchedulesAsync(item, node, options); if (changes.Count != 0) return SyncAttempt.Succeed(item.Name ?? item.Id.ToString(), item, ChangeType.Import, "" ?? string.Empty, true, - [..details, ..changes]); + [.. details, .. changes]); // if we have changed the sort order, then we return a change, else it was no change. return SyncAttempt.Succeed(item.Name ?? item.Id.ToString(), item, @@ -794,4 +798,12 @@ public override Task DeleteItemAsync(IContent item) } }); } + + protected override Task OnKeyChange(IContent item, Guid oldKey, Guid newKey) + { + // key changes need to clean the DocumentUrl cache. + _urlCleaner.CleanUrlsForDocument(oldKey); + return Task.CompletedTask; + } } + diff --git a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs index 8101c79e2..3b53bb4d9 100644 --- a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs +++ b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs @@ -410,6 +410,10 @@ protected virtual async Task> DeserializeBaseAsync(TObj { changes.AddUpdate(uSyncConstants.Xml.Key, item.Key, key); logger.LogTrace("{Id} Setting Key {Key}", item.Id, key); + + if (item.Id > 0) + await OnKeyChange(item, item.Key, key); + item.Key = key; } @@ -426,6 +430,13 @@ protected virtual async Task> DeserializeBaseAsync(TObj return changes; } + protected virtual Task OnKeyChange(TObject item, Guid oldKey, Guid newKey) + { + // nothing to do here, but subclasses might need to act on key changes. + logger.LogDebug("{id} Key changed from {oldKey} to {newKey}", item.Id, oldKey, newKey); + return Task.CompletedTask; + } + protected IEnumerable DeserializeName(TObject item, XElement node, SyncSerializerOptions options) { var nameNode = node.Element(uSyncConstants.Xml.Info)?.Element("NodeName"); diff --git a/uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs index c418f3f17..039346ec0 100644 --- a/uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using uSync.Core.Documents; using uSync.Core.Extensions; using uSync.Core.Mapping; using uSync.Core.Models; @@ -29,8 +30,9 @@ public ContentTemplateSerializer( IContentTypeService contentTypeService, SyncValueMapperCollection syncMappers, IUserService userService, - ITemplateService templateService) - : base(entityService, languageService, relationService, shortStringHelper, logger, contentService, syncMappers, userService, templateService) + ITemplateService templateService, + ISyncDocumentUrlCleaner urlCleaner) + : base(entityService, languageService, relationService, shortStringHelper, logger, contentService, syncMappers, userService, templateService, urlCleaner) { _contentTypeService = contentTypeService; this.umbracoObjectType = UmbracoObjectTypes.DocumentBlueprint; diff --git a/uSync.Core/uSyncCoreBuilderExtensions.cs b/uSync.Core/uSyncCoreBuilderExtensions.cs index ad4a63279..f457c3be9 100644 --- a/uSync.Core/uSyncCoreBuilderExtensions.cs +++ b/uSync.Core/uSyncCoreBuilderExtensions.cs @@ -6,6 +6,7 @@ using uSync.Core.Cache; using uSync.Core.DataTypes; using uSync.Core.Dependency; +using uSync.Core.Documents; using uSync.Core.Mapping; using uSync.Core.Roots.Configs; using uSync.Core.Serialization; @@ -33,6 +34,9 @@ public static IUmbracoBuilder AdduSyncCore(this IUmbracoBuilder builder) builder.Services.AddSingleton(); + // document url cleaner, for key changes + builder.Services.AddSingleton(); + // cache for entity items, we use it to speed up lookups. builder.Services.AddSingleton();