diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index ce087ac7..9e6c2f44 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -233,8 +233,13 @@ template's derived subtree plus its own inheritance chain, tracked and fully inc one load per subtree depth and chain level (Gitea #86). The reconcile is inheritance-only, so every descendant's chain lies inside that set; a template with no children costs a single empty probe query. It still accepts an already-loaded tracked -graph from a caller holding one. The operator `ResyncInheritedMembersAsync` and the -flattener keep reading the whole tracked graph via `GetAllTemplatesAsync`. +graph from a caller holding one. The operator `ResyncInheritedMembersAsync` reads the +same reconcile closure and, when nothing derives from the template (empty closure), +falls back to `GetTemplateChainAsync` — the template's own inheritance chain, tracked, +one load per level — since resyncing a leaf reads only its chain (Gitea #89). The +read-only resolved-members query (`GetResolvedTemplateMembersCommand`, behind the +editor's inherited view and CLI `template resolved-members`) reads that chain alone, +untracked. The flattener keeps reading the whole tracked graph via `GetAllTemplatesAsync`. ### Native Alarm Source Resolution diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs index 89c08489..091f5396 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -83,6 +83,22 @@ public interface ITemplateEngineRepository /// A task that resolves to the subtree followed by the template's chain. Task> GetTemplateReconcileClosureAsync(int templateId, CancellationToken cancellationToken = default); /// + /// Returns the fully-loaded templates (the shape: attributes, + /// alarms, scripts with bodies, compositions, native alarm sources) of + /// 's inheritance chain only — the template, its parent, + /// grandparent, … (see + /// ). + /// That is everything TemplateInheritanceResolver.Resolve reads for one template (Gitea #89): + /// it backs the read-only resolved-members query ( = false) and the + /// inherited-member resync of a template with no descendants (tracked, because the resync writes + /// through these entities). Returns an empty list when the template does not exist. + /// + /// The template whose chain to load. + /// Whether the returned entities are tracked by the context. + /// Cancellation token. + /// A task that resolves to the template followed by its ancestors. + Task> GetTemplateChainAsync(int templateId, bool trackChanges, CancellationToken cancellationToken = default); + /// /// Returns an untracked identity/edge projection of every template (no children), ordered by name. /// /// Cancellation token. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs index 41f52b61..f7ef47a4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs @@ -78,6 +78,44 @@ public static class TemplateReconcileClosure // 2. The root's own inheritance chain, one query per level. A level already loaded as // part of the subtree means a parent cycle — the chain ends there. + await WalkChainAsync(templateId, loaded, result, loadByIds, cancellationToken).ConfigureAwait(false); + return result; + } + + /// + /// Collects only the inheritance chain of : the template itself, + /// its parent, grandparent, … (Gitea #89). This is everything + /// TemplateInheritanceResolver.Resolve reads for one template — the resolver is + /// inheritance-only — so it backs the read-only resolved-members query, and the inherited-member + /// resync of a template nobody derives from. One call per level; + /// a parent cycle or a dangling parent edge ends the chain. + /// + /// The template whose chain to load. + /// Loads the templates with the given ids (unknown ids are omitted). + /// Cancellation token. + /// The template followed by its ancestors, each once; empty when the template is unknown. + public static async Task> CollectChainAsync( + int templateId, + Func, CancellationToken, Task>> loadByIds, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(loadByIds); + var result = new List