From c7774b7ec2052c37c0b8e7453a7e299894f115fe Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 06:25:16 -0400 Subject: [PATCH] perf(templateengine): alarm-override, folder-delete and template-delete checks read lean template projections (review 2026-10-21 04/P6, 08/P1) InstanceService.SetAlarmOverrideAsync and TemplateDeletionService move to the no-tracking, body-free GetAllTemplatesForAnalysisAsync; TemplateFolderService counts folder contents from GetTemplateStubsAsync. --- docs/requirements/Component-TemplateEngine.md | 10 ++++++++- .../Services/InstanceService.cs | 4 +++- .../Services/TemplateDeletionService.cs | 7 ++++-- .../Services/TemplateFolderService.cs | 4 +++- .../Services/InstanceServiceTests.cs | 17 ++++++++------ .../Services/TemplateDeletionServiceTests.cs | 19 +++++++++------- .../Services/TemplateFolderServiceTests.cs | 22 +++++++++++-------- 7 files changed, 54 insertions(+), 29 deletions(-) diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index de279904..c0a44e20 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -211,7 +211,15 @@ The design-time checks — acyclicity (`CycleDetector`), naming collisions (`CollisionDetector`) and canonical-name resolution (`TemplateResolver`) — read the whole template graph but write through none of it and read no script bodies. They use `GetAllTemplatesForAnalysisAsync`: `AsNoTracking`, with `TemplateScript.Code` -projected away. +projected away. The same projection serves the non-analysis lookups that need only +names, locks and edges: `InstanceService.SetAlarmOverrideAsync` (resolving one alarm +name) and `TemplateDeletionService.CanDeleteTemplateAsync` (inheritor / derived / +composer checks). `TemplateFolderService.DeleteFolderAsync`, which only counts +templates per `FolderId`, reads the lighter `GetTemplateStubsAsync` identity projection +(review 2026-10-21 04/P6, 08/P1). Template member add/update paths still load the whole +analysis projection for collision detection; narrowing that to the edited template's +chain + composition closure needs a closure-scoped repository read that does not exist +yet. The tracked, body-bearing `GetAllTemplatesAsync` remains the read for the two paths that genuinely need it — the inheritance reconciler (which compares and copies diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs index 8534f381..c137db3c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs @@ -243,7 +243,9 @@ public class InstanceService // against the template's direct alarms alone would miss them, silently // accepting an override for a non-existent name or bypassing the lock // rule for a composed alarm. Mirrors SetAttributeOverrideAsync. - var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken); + // The resolver reads member names and lock flags only, so the no-tracking, + // script-body-free analysis projection suffices (review 2026-10-21 04/P6, 08/P1). + var allTemplates = await _repository.GetAllTemplatesForAnalysisAsync(cancellationToken); var resolvedAlarm = TemplateResolver .ResolveAllMembers(instance.TemplateId, allTemplates) .FirstOrDefault(m => m.MemberType == "Alarm" && m.CanonicalName == alarmCanonicalName); diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs index a30b8284..8d3db41a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs @@ -53,7 +53,10 @@ public class TemplateDeletionService } // Check 2: Child templates reference it as parent. Split derived vs. regular. - var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken); + // Reads ids, names, parent/derived/owner edges and composition rows only — the + // no-tracking, script-body-free analysis projection carries all of them + // (review 2026-10-21 04/P6, 08/P1). + var allTemplates = await _repository.GetAllTemplatesForAnalysisAsync(cancellationToken); var inheritors = allTemplates.Where(t => t.ParentTemplateId == templateId).ToList(); var regularChildren = inheritors.Where(t => !t.IsDerived).ToList(); var derivatives = inheritors.Where(t => t.IsDerived).ToList(); @@ -79,7 +82,7 @@ public class TemplateDeletionService } // Check 3: Other templates compose it directly. - // Read the Compositions navigation already loaded by GetAllTemplatesAsync + // Read the Compositions navigation already loaded by GetAllTemplatesForAnalysisAsync // rather than issuing one GetCompositionsByTemplateIdAsync round-trip per // template — this is the same source TemplateService // .DeleteTemplateAsync uses for the equivalent check. diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs index 783f565d..a9787413 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs @@ -232,7 +232,9 @@ public class TemplateFolderService return Result.Failure($"Folder with ID {folderId} not found."); var allFolders = await _repository.GetAllFoldersAsync(cancellationToken); - var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken); + // Only FolderId is read — the untracked identity projection, not the full + // template graph with every script body (review 2026-10-21 04/P6, 08/P1). + var allTemplates = await _repository.GetTemplateStubsAsync(cancellationToken); var childFolderCount = allFolders.Count(f => f.ParentFolderId == folderId); var childTemplateCount = allTemplates.Count(t => t.FolderId == folderId); diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs index 09ef518a..1ea7a251 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/InstanceServiceTests.cs @@ -154,7 +154,7 @@ public class InstanceServiceTests var instance = new Instance("Inst1") { Id = 1, TemplateId = 1 }; _repoMock.Setup(r => r.GetInstanceByIdAsync(1, It.IsAny())) .ReturnsAsync(instance); - _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny())) + _repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny())) .ReturnsAsync(new List