diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs
index 0a779a68..ee779405 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs
@@ -70,13 +70,26 @@ public interface ISiteRepository
/// A task that represents the asynchronous operation.
Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default);
- // Instances (for deletion constraint checks)
+ // Instances (for deletion / move constraint checks)
/// Retrieves all instances deployed to a site.
/// The site primary key to filter by.
/// Cancellation token.
/// A task that resolves to a read-only list of entities for the given site.
Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the distinct instances that have at least one
+ /// referencing the given data
+ /// connection. Used as the primary data-integrity guard for moving a
+ /// connection between sites: a bound connection cannot leave its site
+ /// without orphaning the (site-scoped) binding, so the move handler rejects
+ /// the move and names the returned instances as blockers.
+ ///
+ /// The data connection primary key.
+ /// Cancellation token.
+ /// A task that resolves to the distinct referencing entities (empty when none).
+ Task> GetInstancesReferencingDataConnectionAsync(int dataConnectionId, CancellationToken cancellationToken = default);
+
/// Saves all pending changes to the database.
/// Cancellation token.
/// A task that resolves to the number of state entries written to the database.
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs
index 9cbf1065..08881df9 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs
@@ -5,3 +5,18 @@ public record GetDataConnectionCommand(int DataConnectionId);
public record CreateDataConnectionCommand(int SiteId, string Name, string Protocol, string? PrimaryConfiguration, string? BackupConfiguration = null, int FailoverRetryCount = 3);
public record UpdateDataConnectionCommand(int DataConnectionId, string Name, string Protocol, string? PrimaryConfiguration, string? BackupConfiguration = null, int FailoverRetryCount = 3);
public record DeleteDataConnectionCommand(int DataConnectionId);
+
+///
+/// Moves a data connection from its current site to .
+/// Designer-gated and heavily guarded (see the handler): the target site must
+/// exist, the target site must not already own a connection with the same name,
+/// and NO may reference
+/// the connection (instances are site-scoped, so a bound connection cannot leave
+/// its site without orphaning the binding). Name-based native-alarm-source
+/// references (template ConnectionName / instance
+/// ConnectionNameOverride) are also checked and the move is blocked rather
+/// than silently creating an ambiguous/orphaned state. No auto-rewrite is performed.
+///
+/// Primary key of the connection to move.
+/// Primary key of the destination site.
+public record MoveDataConnectionCommand(int DataConnectionId, int TargetSiteId);
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs
index 34499496..ad5d9680 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs
@@ -125,13 +125,37 @@ public class SiteRepository : ISiteRepository
return Task.CompletedTask;
}
- // --- Instances (for deletion constraint checks) ---
+ // --- Instances (for deletion / move constraint checks) ---
///
public async Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
{
return await _dbContext.Instances
.Where(i => i.SiteId == siteId)
+ .Include(i => i.ConnectionBindings)
+ .Include(i => i.NativeAlarmSourceOverrides)
+ .AsSplitQuery()
+ .ToListAsync(cancellationToken);
+ }
+
+ ///
+ public async Task> GetInstancesReferencingDataConnectionAsync(int dataConnectionId, CancellationToken cancellationToken = default)
+ {
+ // The distinct instance ids whose bindings reference the connection.
+ // Bindings are not exposed via their own DbSet on ISiteRepository, so
+ // resolve them through the InstanceConnectionBindings set, then load the
+ // owning instances (cheap; the blocker list is expected to be tiny).
+ var instanceIds = await _dbContext.InstanceConnectionBindings
+ .Where(b => b.DataConnectionId == dataConnectionId)
+ .Select(b => b.InstanceId)
+ .Distinct()
+ .ToListAsync(cancellationToken);
+
+ if (instanceIds.Count == 0)
+ return Array.Empty();
+
+ return await _dbContext.Instances
+ .Where(i => instanceIds.Contains(i.Id))
.ToListAsync(cancellationToken);
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index 3f4a60f2..646080ae 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -187,7 +187,7 @@ public class ManagementActor : ReceiveActor
or DeleteNotificationListCommand
or UpdateSmtpConfigCommand
or CreateDataConnectionCommand or UpdateDataConnectionCommand
- or DeleteDataConnectionCommand
+ or DeleteDataConnectionCommand or MoveDataConnectionCommand
or AddTemplateAttributeCommand or UpdateTemplateAttributeCommand or DeleteTemplateAttributeCommand
or AddTemplateAlarmCommand or UpdateTemplateAlarmCommand or DeleteTemplateAlarmCommand
or AddTemplateNativeAlarmSourceCommand or UpdateTemplateNativeAlarmSourceCommand or DeleteTemplateNativeAlarmSourceCommand
@@ -306,6 +306,7 @@ public class ManagementActor : ReceiveActor
CreateDataConnectionCommand cmd => await HandleCreateDataConnection(sp, cmd, user.Username),
UpdateDataConnectionCommand cmd => await HandleUpdateDataConnection(sp, cmd, user.Username),
DeleteDataConnectionCommand cmd => await HandleDeleteDataConnection(sp, cmd, user.Username),
+ MoveDataConnectionCommand cmd => await HandleMoveDataConnection(sp, cmd, user.Username),
// External Systems
ListExternalSystemsCommand => await HandleListExternalSystems(sp),
@@ -1373,6 +1374,108 @@ public class ManagementActor : ReceiveActor
return true;
}
+ ///
+ /// Moves a data connection to another site (M9 / T24a). High-risk + data
+ /// integrity: every guard runs server-side BEFORE the write, and when in
+ /// doubt the move is BLOCKED with a clear error rather than risking an
+ /// orphaned binding/reference. No bindings or name references are rewritten.
+ ///
+ private static async Task