fix(transport): reject bundle imports that would persist an inheritance/composition cycle
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -18,6 +18,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Encryption;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
||||
@@ -1044,6 +1045,19 @@ public sealed class BundleImporter : IBundleImporter
|
||||
await ResolveCompositionEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
|
||||
await ResolveInheritanceEdgesAsync(content.Templates, resolutionMap, user, ct).ConfigureAwait(false);
|
||||
|
||||
// ---- Import-time acyclicity guard over the merged template graph ----
|
||||
// The two rewire passes above have STAGED (not yet saved) the bundle's
|
||||
// inheritance (ParentTemplateId) and composition edges onto the tracked
|
||||
// target templates. A bundle can still close a loop through a pre-existing
|
||||
// target template — e.g. Overwrite a base so it now inherits from (or
|
||||
// composes) one of its own descendants. The design-time save-gate enforces
|
||||
// acyclicity on individual edits, but an import merges many edges at once,
|
||||
// so re-check the whole merged graph here. On a cycle we throw and the
|
||||
// catch below rolls the whole import back (ChangeTracker.Clear on the
|
||||
// in-memory provider) BEFORE any of it is persisted; deliberately no
|
||||
// SaveChanges precedes this check so nothing survives that rollback.
|
||||
EnsureNoTemplateGraphCycles();
|
||||
|
||||
// ---- Site/instance-scoped apply: instances LAST ----
|
||||
// The instance pass needs every FK target materialised first:
|
||||
// • template id — resolved by name against the just-flushed central
|
||||
@@ -2178,6 +2192,60 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post-rewire acyclicity guard: reads the fully-merged template graph off the
|
||||
/// change tracker (staged, not-yet-saved edges included) and rejects the import
|
||||
/// if the bundle's inheritance or composition edges would persist a cycle —
|
||||
/// e.g. an Overwrite that makes a base inherit from, or compose, its own
|
||||
/// descendant. Delegates to the authoritative TemplateEngine
|
||||
/// <see cref="CycleDetector"/>. Throws <see cref="SemanticValidationException"/>
|
||||
/// on the first cycle found; the caller's catch rolls the whole import back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The template list is taken from the change tracker rather than a fresh query
|
||||
/// so it reflects the staged edges without a premature flush. It is complete for
|
||||
/// cycle detection: any edge that references a pre-existing target template forces
|
||||
/// a full <c>GetAllTemplatesAsync</c> load in the rewire passes (tracking every
|
||||
/// template), and any all-imported cycle consists solely of tracked (Local)
|
||||
/// templates — so every node of any candidate cycle is present.
|
||||
/// </remarks>
|
||||
private void EnsureNoTemplateGraphCycles()
|
||||
{
|
||||
var templates = _dbContext.ChangeTracker.Entries<Template>()
|
||||
.Where(e => e.State != EntityState.Deleted)
|
||||
.Select(e => e.Entity)
|
||||
.ToList();
|
||||
if (templates.Count == 0) return;
|
||||
|
||||
foreach (var t in templates)
|
||||
{
|
||||
if (t.ParentTemplateId.HasValue)
|
||||
{
|
||||
var cycle = CycleDetector.DetectInheritanceCycle(t.Id, t.ParentTemplateId.Value, templates);
|
||||
if (cycle is not null)
|
||||
{
|
||||
throw new SemanticValidationException(new[]
|
||||
{
|
||||
$"Import would create a template inheritance cycle: {cycle}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var comp in t.Compositions)
|
||||
{
|
||||
if (_dbContext.Entry(comp).State == EntityState.Deleted) continue;
|
||||
var cycle = CycleDetector.DetectCompositionCycle(t.Id, comp.ComposedTemplateId, templates);
|
||||
if (cycle is not null)
|
||||
{
|
||||
throw new SemanticValidationException(new[]
|
||||
{
|
||||
$"Import would create a template composition cycle: {cycle}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplySharedScriptsAsync(
|
||||
IReadOnlyList<SharedScriptDto> dtos,
|
||||
Dictionary<(string, string), ImportResolution> map,
|
||||
|
||||
Reference in New Issue
Block a user