From 1361a39770a06ab8bbb5b2f038e2983edebc1a6c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 24 May 2026 08:20:34 -0400 Subject: [PATCH] fix(cli): dedupe import resolutions before ApplyAsync PreviewAsync can emit multiple ImportPreviewItem rows for the same (EntityType, Name) -- one per modified member of a template, for example. ApplyAsync internally calls .ToDictionary() on the resolutions list and throws ArgumentException on duplicate keys. The Central UI's BuildDefaultResolutions already dedupes via a dictionary assignment (last-write-wins). Mirror that in the CLI handler so 'bundle import' tolerates the duplicate-rows shape the preview returns. --- .../ManagementActor.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ScadaLink.ManagementService/ManagementActor.cs b/src/ScadaLink.ManagementService/ManagementActor.cs index 8b69783..e259012 100644 --- a/src/ScadaLink.ManagementService/ManagementActor.cs +++ b/src/ScadaLink.ManagementService/ManagementActor.cs @@ -1838,22 +1838,29 @@ public class ManagementActor : ReceiveActor $"Bundle has {blockers.Count} blocker(s); import aborted. {details}"); } + // Dedupe by (EntityType, Name) -- the preview can emit multiple rows per + // entity (e.g. one row per modified member of a template), but ApplyAsync + // requires a unique resolution per key. Last-write-wins matches the + // Central UI's TransportImport.BuildDefaultResolutions behavior. var renameStamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"); - var resolutions = preview.Items.Select(item => new ImportResolution( - item.EntityType, - item.Name, - item.Kind switch + var resolutionsMap = new Dictionary<(string, string), ImportResolution>(); + foreach (var item in preview.Items) + { + var action = item.Kind switch { ConflictKind.New => ResolutionAction.Add, ConflictKind.Identical => ResolutionAction.Skip, ConflictKind.Modified => policy, _ => ResolutionAction.Skip, - }, - (item.Kind == ConflictKind.Modified && policy == ResolutionAction.Rename) + }; + var renameTo = (item.Kind == ConflictKind.Modified && policy == ResolutionAction.Rename) ? $"{item.Name}-imported-{renameStamp}" - : null)).ToList(); + : null; + resolutionsMap[(item.EntityType, item.Name)] = new ImportResolution( + item.EntityType, item.Name, action, renameTo); + } - return await importer.ApplyAsync(session.SessionId, resolutions, username); + return await importer.ApplyAsync(session.SessionId, resolutionsMap.Values.ToList(), username); } private static byte[] DecodeBundle(string base64)