perf(templates): member add/update loads only the chain + composition closure (ScadaBridge#66)
Attribute/alarm/script add (collision check) and update (parent-lock lookup) used GetAllTemplatesForAnalysisAsync, materialising every template, attribute, alarm and script in the library per edit. They now call the new ITemplateEngineRepository.GetTemplateAnalysisClosureAsync: the same untracked, body-free analysis projection, restricted to the template's inheritance chain plus the composition closure of every chain member - exactly what TemplateResolver and CollisionDetector walk (a composed module's own parent chain is never climbed, so it is not loaded). Adds use the edited template's closure, updates the parent's. The walk (Commons TemplateAnalysisClosure) loads one batch per closure depth, requests each id once, terminates on cycles and skips dangling edges. Evidence: - TemplateAnalysisClosureTests: for every template of a graph with chains, nested + diamond compositions, a derived module, cycles, dangling edges and colliding unrelated templates, ResolveAllMembers / DetectCollisions / FindMemberByCanonicalName answer identically from the closure and the whole graph (mutation-checked). - TemplateAnalysisClosureRepositoryTests (SQLite, recorded SQL): 6 templates / 12 attributes loaded instead of 47 / 94, in 4 queries (one per depth), untracked, script bodies never selected. - TemplateServiceTests: two new tests with the whole-graph load wired to throw (fail on the old service); every existing add/update test now runs against the closure via AnalysisClosureMock. ReconcileDescendantsAsync still loads the tracked full graph; not in scope. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+28
-4
@@ -122,8 +122,33 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
// <see cref="GetAllTemplatesAsync"/> / <see cref="GetTemplateWithChildrenAsync"/>.
|
||||
public async Task<IReadOnlyList<Template>> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
.AsNoTracking()
|
||||
return await AnalysisProjection(_context.Templates.AsNoTracking())
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// One projection query per closure depth (TemplateAnalysisClosure batches each frontier),
|
||||
// each restricted to the frontier's ids — so a member edit reads its own chain and modules
|
||||
// instead of every template, attribute, alarm and script in the library.
|
||||
public Task<IReadOnlyList<Template>> GetTemplateAnalysisClosureAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return TemplateAnalysisClosure.CollectAsync(
|
||||
templateId,
|
||||
async (ids, ct) =>
|
||||
{
|
||||
var idList = ids.ToList();
|
||||
return await AnalysisProjection(_context.Templates.AsNoTracking().Where(t => idList.Contains(t.Id)))
|
||||
.ToListAsync(ct);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// The analysis projection shared by GetAllTemplatesForAnalysisAsync and
|
||||
// GetTemplateAnalysisClosureAsync (see the note on the former for why it is untracked and
|
||||
// drops TemplateScript.Code).
|
||||
private static IQueryable<Template> AnalysisProjection(IQueryable<Template> source)
|
||||
{
|
||||
return source
|
||||
.Select(t => new Template(t.Name)
|
||||
{
|
||||
Id = t.Id,
|
||||
@@ -152,8 +177,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
LockedInDerived = s.LockedInDerived
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user