From 5014d51b0ffb44591aba4e5c4dbf2b6ca9e1c932 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Wed, 19 Aug 2026 14:15:47 +0100 Subject: [PATCH 1/2] Surface the real TemplateOperationStatus when template creation fails Template import failures (#1044) reported a generic "Failed to create template" with a null exception, hiding the actual Umbraco status (e.g. duplicate alias) - especially hard to diagnose on IIS/Production where the failure mode differs from local dev. Propagate attempt.Status and wrap it in an exception so the sync result carries enough detail to diagnose. Also removes an unreachable duplicate null check left over from an earlier refactor, and adds coverage for both the failure and success create paths. Co-Authored-By: Claude Sonnet 5 --- .../Serializers/TemplateSerializer.cs | 13 +- .../Serializers/TemplateSerializerTests.cs | 141 ++++++++++++++++++ 2 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 uSync.Tests/Serializers/TemplateSerializerTests.cs diff --git a/uSync.Core/Serialization/Serializers/TemplateSerializer.cs b/uSync.Core/Serialization/Serializers/TemplateSerializer.cs index 3061abef..c2665d7a 100644 --- a/uSync.Core/Serialization/Serializers/TemplateSerializer.cs +++ b/uSync.Core/Serialization/Serializers/TemplateSerializer.cs @@ -102,8 +102,10 @@ protected override async Task> DeserializeCoreAsync(XElem contentAttempt.Result, userKey, key); - if (attempt.Success is false) - return SyncAttempt.Fail(name, ChangeType.Import, "Failed to create template"); + if (attempt.Success is false) + return SyncAttempt.Fail(name, attempt.Result, ChangeType.Import, + $"Failed to create template: {attempt.Status}", + new InvalidOperationException($"Failed to create template '{alias}': {attempt.Status} {attempt.Exception?.Message ?? "Unknown error"}")); item = attempt.Result; details.AddNew(alias, alias, "Template"); @@ -115,13 +117,6 @@ protected override async Task> DeserializeCoreAsync(XElem return SyncAttempt.Succeed(name, item, ChangeType.Import, "Created", true, details); } - if (item is null) - { - // creating went wrong - logger.LogWarning("Failed to create template"); - return SyncAttempt.Fail(name, ChangeType.Import, "Failed to create template"); - } - if (item.Key != key) { details.AddUpdate(uSyncConstants.Xml.Key, item.Key, key); diff --git a/uSync.Tests/Serializers/TemplateSerializerTests.cs b/uSync.Tests/Serializers/TemplateSerializerTests.cs new file mode 100644 index 00000000..2cb803f3 --- /dev/null +++ b/uSync.Tests/Serializers/TemplateSerializerTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Threading.Tasks; +using System.Xml.Linq; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +using Moq; + +using NUnit.Framework; + +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; + +using uSync.Core; +using uSync.Core.Models; +using uSync.Core.Serialization; +using uSync.Core.Serialization.Serializers; + +namespace uSync.Tests.Serializers; + +/// +/// guards against #1044 - a failed template create used to be reported as a +/// hardcoded "Failed to create template" with no exception/status, hiding +/// why the import failed (e.g. only reproducible on IIS/Production). +/// +[TestFixture] +public class TemplateSerializerTests +{ + private Mock _templateServiceMock; + private Mock _userIdKeyResolverMock; + private TemplateSerializer _serializer; + + [SetUp] + public void Setup() + { + _templateServiceMock = new Mock(); + _userIdKeyResolverMock = new Mock(); + _userIdKeyResolverMock.Setup(x => x.GetAsync(It.IsAny())).ReturnsAsync(Guid.NewGuid()); + + var versionMock = new Mock(); + versionMock.Setup(x => x.Version).Returns(new Version(17, 3, 0)); + + var fileSystems = BuildFileSystems(); + + _serializer = new TemplateSerializer( + Mock.Of(), + NullLogger.Instance, + Mock.Of(), + fileSystems, + new ConfigurationBuilder().Build(), + new uSyncCapabilityChecker(versionMock.Object), + _templateServiceMock.Object, + _userIdKeyResolverMock.Object); + } + + private static FileSystems BuildFileSystems() + { + var hostingEnvironment = new Mock(); +#pragma warning disable CS0618 // used only to satisfy FileSystems' internal setup + hostingEnvironment.Setup(x => x.MapPathContentRoot(It.IsAny())) + .Returns(path => "C:/temp/" + path.TrimStart('~', '/')); + hostingEnvironment.Setup(x => x.MapPathWebRoot(It.IsAny())) + .Returns(path => "C:/temp/" + path.TrimStart('~', '/')); +#pragma warning restore CS0618 + hostingEnvironment.Setup(x => x.ToAbsolute(It.IsAny())) + .Returns(path => "/" + path.TrimStart('~', '/')); + + var ioHelper = new Mock(); +#pragma warning disable CS0618 // used only to satisfy FileSystems' internal setup + ioHelper.Setup(x => x.ResolveUrl(It.IsAny())).Returns(path => "/" + path.TrimStart('~', '/')); +#pragma warning restore CS0618 + + return new FileSystems( + Mock.Of(), + ioHelper.Object, + Options.Create(new GlobalSettings()), + hostingEnvironment.Object); + } + + private static XElement BuildTemplateNode(Guid key, string alias, string name) + => new XElement("Template", + new XAttribute(uSyncConstants.Xml.Key, key), + new XAttribute(uSyncConstants.Xml.Alias, alias), + new XElement("Name", name), + new XElement("Contents", new XCData("@{ Layout = null; }"))); + + [Test] + public async Task Create_WhenTemplateServiceFails_ReturnsStatusAndException() + { + // arrange: template doesn't exist locally, so the serializer will try to create it, + // and Umbraco's ITemplateService reports it couldn't be created (e.g. duplicate alias). + _templateServiceMock.Setup(x => x.GetAsync(It.IsAny())).ReturnsAsync((ITemplate)null); + _templateServiceMock.Setup(x => x.GetAsync(It.IsAny())).ReturnsAsync((ITemplate)null); + _templateServiceMock + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(Attempt.Fail(TemplateOperationStatus.DuplicateAlias)); + + var node = BuildTemplateNode(Guid.NewGuid(), "linkTreeNew", "LinkTreeNew"); + + // act + var result = await _serializer.DeserializeAsync(node, new SyncSerializerOptions()); + + // assert: the real Umbraco status and an exception now flow through, instead of a + // generic message with no way to diagnose the underlying cause. + Assert.Multiple(() => + { + Assert.That(result.Success, Is.False); + Assert.That(result.Message, Does.Contain(nameof(TemplateOperationStatus.DuplicateAlias))); + Assert.That(result.Exception, Is.Not.Null); + Assert.That(result.Exception.Message, Does.Contain(nameof(TemplateOperationStatus.DuplicateAlias))); + }); + } + + [Test] + public async Task Create_WhenTemplateServiceSucceeds_ReturnsSucceededAttempt() + { + _templateServiceMock.Setup(x => x.GetAsync(It.IsAny())).ReturnsAsync((ITemplate)null); + _templateServiceMock.Setup(x => x.GetAsync(It.IsAny())).ReturnsAsync((ITemplate)null); + + var created = new Template(Mock.Of(), "LinkTreeNew", "linkTreeNew"); + _templateServiceMock + .Setup(x => x.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(Attempt.Succeed(TemplateOperationStatus.Success, created)); + + var node = BuildTemplateNode(Guid.NewGuid(), "linkTreeNew", "LinkTreeNew"); + + var result = await _serializer.DeserializeAsync(node, new SyncSerializerOptions()); + + Assert.That(result.Success, Is.True); + } +} From 6217f49e9d420d95e6e0a69399d80f9c2a7ec8c9 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Tue, 25 Aug 2026 16:42:06 +0100 Subject: [PATCH 2/2] Add non-breaking async callback surface to uSyncCallbacks (#1049) The uSyncCallbacks delegates (Callback, Update, SetRange, IncrementalUpdate, Complete) were void, which forced consumers like uSync.Complete's LocalHubClient/PublisherHubClient to .Wait() on every SignalR send. Add optional async counterparts (CallbackAsync, UpdateAsync, SetRangeAsync, IncrementalUpdateAsync, CompleteAsync) alongside the existing sync delegates, plus Raise*Async helper methods that invoke the sync delegate then await the async one if set. Existing constructors, properties and delegate types are untouched, so this is purely additive. Update the call sites that invoke callbacks directly off a uSyncCallbacks instance (SyncService, SyncService_Single, SyncActionService, SyncHandlerRoot, uSyncManagementService) to await the new Raise*Async helpers. Call sites that extract the raw SyncUpdateCallback delegate to pass into the public ISyncHandler.ExportAllAsync/ReportAsync interface are left untouched, since changing that interface would be a breaking change. --- uSync.BackOffice/Hubs/uSyncCallbacks.cs | 117 +++++++++++++++++- .../Services/SyncActionService.cs | 6 +- uSync.BackOffice/Services/SyncService.cs | 22 ++-- .../Services/SyncService_Single.cs | 21 ++-- .../SyncHandlers/SyncHandlerRoot.cs | 18 ++- .../Services/uSyncManagementService.cs | 15 ++- 6 files changed, 166 insertions(+), 33 deletions(-) diff --git a/uSync.BackOffice/Hubs/uSyncCallbacks.cs b/uSync.BackOffice/Hubs/uSyncCallbacks.cs index 6f6fdfaf..77b0bf36 100644 --- a/uSync.BackOffice/Hubs/uSyncCallbacks.cs +++ b/uSync.BackOffice/Hubs/uSyncCallbacks.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using uSync.BackOffice.Models; @@ -11,26 +12,52 @@ namespace uSync.BackOffice; public delegate void SyncEventCallback(SyncProgressSummary summary); /// -/// callback delegate for SignalR messaging +/// async callback delegate for SignalR messaging +/// +public delegate Task SyncEventCallbackAsync(SyncProgressSummary summary); + +/// +/// callback delegate for SignalR messaging /// public delegate void SyncUpdateCallback(string message, int count, int total); +/// +/// async callback delegate for SignalR messaging +/// +public delegate Task SyncUpdateCallbackAsync(string message, int count, int total); + /// /// callback delegate to set the start and end range for the update counters. /// public delegate void SyncSetUpdateRange(int start, int end); +/// +/// async callback delegate to set the start and end range for the update counters. +/// +public delegate Task SyncSetUpdateRangeAsync(int start, int end); + /// /// callback to send a update message and increment the counter by one so moving the progress bar. /// /// public delegate void SyncIncrementalUpdateCallback(string message); +/// +/// async callback to send a update message and increment the counter by one so moving the progress bar. +/// +/// +public delegate Task SyncIncrementalUpdateCallbackAsync(string message); + /// /// callback to signal that the sync is complete /// public delegate void SyncCompleteCallBack(Guid id, string message, bool success, IEnumerable actions); +/// +/// async callback to signal that the sync is complete +/// +public delegate Task SyncCompleteCallBackAsync(Guid id, string message, bool success, IEnumerable actions); + /// /// Callback objects used to communicate via SignalR @@ -42,29 +69,56 @@ public class uSyncCallbacks /// public SyncEventCallback? Callback { get; private set; } + /// + /// Async add event callback. Set alongside (or instead of) when the + /// consumer needs to await the send (e.g. a SignalR hub push) rather than block on it. + /// + public SyncEventCallbackAsync? CallbackAsync { get; set; } + /// /// Update event callback /// public SyncUpdateCallback? Update { get; private set; } + /// + /// Async update event callback. Set alongside (or instead of) when the + /// consumer needs to await the send rather than block on it. + /// + public SyncUpdateCallbackAsync? UpdateAsync { get; set; } + /// /// set a start and end range for the counter. /// public SyncSetUpdateRange? SetRange { get; private set; } + /// + /// Async version of . + /// + public SyncSetUpdateRangeAsync? SetRangeAsync { get; set; } + /// /// update and increment callback. /// public SyncIncrementalUpdateCallback? IncrementalUpdate { get; private set; } + /// + /// Async version of . + /// + public SyncIncrementalUpdateCallbackAsync? IncrementalUpdateAsync { get; set; } + /// /// callback to signal that the sync is complete /// public SyncCompleteCallBack? Complete { get; private set; } + /// + /// Async version of . + /// + public SyncCompleteCallBackAsync? CompleteAsync { get; set; } + /// - /// generate a new callback object + /// generate a new callback object /// public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) { @@ -75,9 +129,9 @@ public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) /// /// generate a callback object with range and incremental update /// - public uSyncCallbacks(SyncEventCallback? callback, + public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update, - SyncSetUpdateRange? updateRange, + SyncSetUpdateRange? updateRange, SyncIncrementalUpdateCallback incrementalUpdate, SyncCompleteCallBack? complete) : this(callback, update) @@ -86,4 +140,59 @@ public uSyncCallbacks(SyncEventCallback? callback, this.IncrementalUpdate = incrementalUpdate; this.Complete = complete; } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseCallbackAsync(SyncProgressSummary summary) + { + Callback?.Invoke(summary); + if (CallbackAsync is not null) + await CallbackAsync(summary).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseUpdateAsync(string message, int count, int total) + { + Update?.Invoke(message, count, total); + if (UpdateAsync is not null) + await UpdateAsync(message, count, total).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseSetRangeAsync(int start, int end) + { + SetRange?.Invoke(start, end); + if (SetRangeAsync is not null) + await SetRangeAsync(start, end).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting + /// the async version (if set) so callers no longer need to block on it. + /// + public async Task RaiseIncrementalUpdateAsync(string message) + { + IncrementalUpdate?.Invoke(message); + if (IncrementalUpdateAsync is not null) + await IncrementalUpdateAsync(message).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseCompleteAsync(Guid id, string message, bool success, IEnumerable actions) + { + Complete?.Invoke(id, message, success, actions); + if (CompleteAsync is not null) + await CompleteAsync(id, message, success, actions).ConfigureAwait(false); + } } diff --git a/uSync.BackOffice/Services/SyncActionService.cs b/uSync.BackOffice/Services/SyncActionService.cs index 6c7441c4..85c815e6 100644 --- a/uSync.BackOffice/Services/SyncActionService.cs +++ b/uSync.BackOffice/Services/SyncActionService.cs @@ -123,7 +123,8 @@ public async Task ImportPostAsync(SyncFinalActionRequest reque request.ActionOptions.GetSetOrDefault(_uSyncConfig.Settings.DefaultSet), request.Actions); - request.Callbacks?.Update?.Invoke("Post Import Complete", 1, 1); + if (request.Callbacks is not null) + await request.Callbacks.RaiseUpdateAsync("Post Import Complete", 1, 1); return new SyncActionResult([.. actions.Where(x => x.Change > Core.ChangeType.NoChange)]); } @@ -217,7 +218,8 @@ public async Task FinishProcessAsync(SyncFinalActionRequest re request.HandlerAction, request.Actions.CountChanges(), request.Actions.Count(), elapsed); } - request.Callbacks?.Update?.Invoke($"{request.HandlerAction} completed ({elapsed:#,#}ms)", 1, 1); + if (request.Callbacks is not null) + await request.Callbacks.RaiseUpdateAsync($"{request.HandlerAction} completed ({elapsed:#,#}ms)", 1, 1); // for speed we return an empty list. the merge will just take // what we where passed in, and we avoid a whole copy and compare step diff --git a/uSync.BackOffice/Services/SyncService.cs b/uSync.BackOffice/Services/SyncService.cs index 8818f32a..ddefee10 100644 --- a/uSync.BackOffice/Services/SyncService.cs +++ b/uSync.BackOffice/Services/SyncService.cs @@ -197,7 +197,8 @@ public async Task> ImportAsync(string[] folders, bool f summary.UpdateHandler( handler.Name, HandlerStatus.Processing, $"Importing {handler.Name}", 0); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); var handlerActions = await this.ImportHandlerAsync(handler.Alias, importOptions); @@ -227,14 +228,18 @@ public async Task> ImportAsync(string[] folders, bool f if (actions.ContainsErrors()) _logger.LogWarning("uSync Import: Errors detected in import : {count}", actions.CountErrors()); - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + if (callbacks is not null) + await callbacks.RaiseUpdateAsync($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); summary.Message = "Completed"; summary.Total = handlers.Count() + 1; var finalActions = actions.Select(x => x.AsActionView()).ToList(); - callbacks?.Callback?.Invoke(summary); - callbacks?.Complete?.Invoke(requestId, "Sync complete", true, finalActions); + if (callbacks is not null) + { + await callbacks.RaiseCallbackAsync(summary); + await callbacks.RaiseCompleteAsync(requestId, "Sync complete", true, finalActions); + } return actions; } @@ -386,7 +391,8 @@ public async Task> ExportAsync(string folder, IEnumerab summary.UpdateHandler( handler.Name, HandlerStatus.Processing, $"Exporting {handler.Name}", 0); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); var handlerActions = await handler.ExportAllAsync([$"{folder}/{handler.DefaultFolder}"], configuredHandler.Settings, callbacks?.Update); @@ -398,7 +404,8 @@ public async Task> ExportAsync(string folder, IEnumerab } summary.UpdateMessage("Export Completed"); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); await _mutexService.FireBulkCompleteAsync(new uSyncExportCompletedNotification(actions, null)); @@ -412,7 +419,8 @@ public async Task> ExportAsync(string folder, IEnumerab sw.ElapsedMilliseconds); } - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + if (callbacks is not null) + await callbacks.RaiseUpdateAsync($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); return actions; } diff --git a/uSync.BackOffice/Services/SyncService_Single.cs b/uSync.BackOffice/Services/SyncService_Single.cs index f9b0b341..6f018838 100644 --- a/uSync.BackOffice/Services/SyncService_Single.cs +++ b/uSync.BackOffice/Services/SyncService_Single.cs @@ -59,8 +59,9 @@ public async Task> ReportPartialAsync(IList> ImportPartialAsync(IList> ImportPartialSecondPassAsync(IEnumer continue; } - options.Callbacks?.Update?.Invoke($"Second Pass: {action.Name}", - CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync($"Second Pass: {action.Name}", + CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); secondPassActions.AddRange(await handlerPair.Handler.ImportSecondPassAsync(action, handlerPair.Settings, options)); @@ -273,7 +276,8 @@ public async Task> ImportPartialPostImportAsync(IEnumer { if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, folders.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync(actionItem.alias, index, folders.Count); var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); results.AddRange(await postImportHandler.ProcessPostImportAsync(handlerActions, handlerPair.Settings)); @@ -326,7 +330,8 @@ public async Task> ImportPostCleanFilesAsync(IEnumerabl if (handlerPair.Handler is ISyncCleanEntryHandler cleanEntryHandler) { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, cleans.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync(actionItem.alias, index, cleans.Count); var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); results.AddRange(await cleanEntryHandler.ProcessCleanActionsAsync(actionItem.folder, handlerActions, handlerPair.Settings)); diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index 72855144..80a76f86 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -270,11 +270,13 @@ public async Task> ImportAllAsync(string[] folders, Han var cacheKey = PrepCaches(); try { - options.Callbacks?.Update?.Invoke("Calculating import order", 1, 9); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync("Calculating import order", 1, 9); var items = await GetMergedItemsAsync(folders, new SyncMergeOptions(options.Callbacks?.Update)); - options.Callbacks?.Update?.Invoke($"Processing {items.Count} items", 2, 9); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync($"Processing {items.Count} items", 2, 9); // create the update list with items.count space. this is the max size we need this list. List actions = new(items.Count); @@ -284,7 +286,8 @@ public async Task> ImportAllAsync(string[] folders, Han int count = 0; int total = items.Count; - options.Callbacks?.SetRange?.Invoke(count, total); + if (options.Callbacks is not null) + await options.Callbacks.RaiseSetRangeAsync(count, total); foreach (var item in items) { @@ -330,7 +333,8 @@ public async Task> ImportAllAsync(string[] folders, Han await PerformImportCleanAsync(cleanMarkers, actions, config, options.Callbacks?.Update); } - options.Callbacks?.Update?.Invoke("Done", 3, 3); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync("Done", 3, 3); if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug("ImportAll: {count} items imported", actions.Count); @@ -462,7 +466,8 @@ virtual public async Task> ImportElementAsync(XElement { var actions = new List(); var elements = node.Elements().ToList(); - options.Callbacks?.SetRange?.Invoke(0, elements.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseSetRangeAsync(0, elements.Count); foreach (var item in elements) { actions.AddRange(await ImportSingleElementAsync(new XElement(item), filename, settings, options)); @@ -502,7 +507,8 @@ virtual protected async Task> ImportSingleElementAsync( try { - options.Callbacks?.IncrementalUpdate?.Invoke(node.GetAlias()); + if (options.Callbacks is not null) + await options.Callbacks.RaiseIncrementalUpdateAsync(node.GetAlias()); // merge the options from the handler and any import options into our serializer options. var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId); diff --git a/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs b/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs index 21e99b19..d95befa4 100644 --- a/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs +++ b/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs @@ -268,7 +268,7 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest if (actionRequest.StepNumber >= handlers.Count) { var actions = await PerformFinalSteps(requestId, action, handlerOptions, callbacks, user?.Username); - return SummerizeCompleteProcess(actionRequest, action, handlers, requestId, callbacks, actions); + return await SummerizeCompleteProcess(actionRequest, action, handlers, requestId, callbacks, actions); } @@ -281,7 +281,7 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest var allActions = _syncManagementCache.GetCachedActions(requestId); var summaries = GetSummaries(action, handlers, actionRequest.StepNumber, allActions); - callbacks.Callback?.Invoke(new SyncProgressSummary(summaries, "Processing " + action.ToString(), handlers.Count)); + await callbacks.RaiseCallbackAsync(new SyncProgressSummary(summaries, "Processing " + action.ToString(), handlers.Count)); return new PerformActionResponse { @@ -292,15 +292,18 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest }; } - private static PerformActionResponse SummerizeCompleteProcess(PerformActionRequest actionRequest, HandlerActions action, List handlers, Guid requestId, uSyncCallbacks callbacks, List actions) + private static async Task SummerizeCompleteProcess(PerformActionRequest actionRequest, HandlerActions action, List handlers, Guid requestId, uSyncCallbacks callbacks, List actions) { var finalSummary = GetSummaries(action, handlers, actionRequest.StepNumber + 1, actions); var actionViews = actions.Select(x => x.AsActionView()); - callbacks?.Callback?.Invoke(new SyncProgressSummary(finalSummary, "Completed", handlers.Count)); - callbacks?.Complete?.Invoke(requestId, "Sync complete", true, actionViews); + if (callbacks is not null) + { + await callbacks.RaiseCallbackAsync(new SyncProgressSummary(finalSummary, "Completed", handlers.Count)); + await callbacks.RaiseCompleteAsync(requestId, "Sync complete", true, actionViews); + } - // finished. + // finished. return new PerformActionResponse { RequestId = requestId.ToString(),