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();
+ await WalkChainAsync(templateId, new Dictionary(), result, loadByIds, cancellationToken)
+ .ConfigureAwait(false);
+ return result;
+ }
+
+ // Walks templateId's parent chain upward, loading each level not already in `loaded` (one
+ // loadByIds call per level) and appending it to `result`. Ends on a repeated id (a parent
+ // cycle) or a template that does not exist (a dangling parent edge, or an unknown root).
+ private static async Task WalkChainAsync(
+ int templateId,
+ Dictionary loaded,
+ List result,
+ Func, CancellationToken, Task>> loadByIds,
+ CancellationToken cancellationToken)
+ {
var requested = new HashSet();
int? nextId = templateId;
while (nextId is int id && requested.Add(id))
@@ -97,7 +135,5 @@ public static class TemplateReconcileClosure
result.Add(template);
nextId = template.ParentTemplateId;
}
-
- return result;
}
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs
index b398b9a3..ba088f47 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs
@@ -161,8 +161,26 @@ public class TemplateEngineRepository : ITemplateEngineRepository
cancellationToken);
}
- // The tracked, fully-included template shape shared by GetAllTemplatesAsync and
- // GetTemplateReconcileClosureAsync.
+ ///
+ // One fully-included query per chain level (Gitea #89). Chains are short (a handful of
+ // levels), so this is a few small reads instead of the whole library.
+ public Task> GetTemplateChainAsync(int templateId, bool trackChanges, CancellationToken cancellationToken = default)
+ {
+ return TemplateReconcileClosure.CollectChainAsync(
+ templateId,
+ async (ids, ct) =>
+ {
+ var idList = ids.ToList();
+ var query = FullTemplateGraph().Where(t => idList.Contains(t.Id));
+ if (!trackChanges)
+ query = query.AsNoTracking();
+ return await query.ToListAsync(ct);
+ },
+ cancellationToken);
+ }
+
+ // The tracked, fully-included template shape shared by GetAllTemplatesAsync,
+ // GetTemplateReconcileClosureAsync and GetTemplateChainAsync.
private IQueryable FullTemplateGraph()
{
return _context.Templates
diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index de28e0af..b3875b1c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -869,15 +869,15 @@ public class ManagementActor : ReceiveActor
///
/// Read-only authoring resolve: returns the effective inherited
/// member set for a template — computed fresh from the full inheritance
- /// chain — plus a staleness summary. Loads every template (children
- /// eager-loaded) so the resolver can walk an arbitrary-depth chain; never
- /// mutates stored rows.
+ /// chain — plus a staleness summary. The resolver is inheritance-only, so it
+ /// loads just the template's own chain, untracked (Gitea #89) — never the
+ /// whole library; never mutates stored rows.
///
private static async Task