fix(deployment): flatten reads compositions off the loaded template instead of a second per-template query (review 2026-10-21 04/U5)

This commit is contained in:
Joseph Doherty
2026-10-22 06:32:32 -04:00
parent 669822772e
commit 7975699ac4
6 changed files with 25 additions and 8 deletions
@@ -240,9 +240,14 @@ public class FlatteningPipeline : IFlatteningPipeline
{
if (!processedTemplateIds.Add(template.Id)) continue;
// The chain's templates come from GetTemplateWithChildrenAsync, which already
// Includes Compositions — read them off the loaded entity rather than issuing a
// second per-template composition query.
var loaded = template;
var compositions = await session.GetCompositionsAsync(
template.Id,
id => _templateRepo.GetCompositionsByTemplateIdAsync(id, cancellationToken));
_ => Task.FromResult<IReadOnlyList<Commons.Entities.Templates.TemplateComposition>>(
loaded.Compositions.ToList()));
if (compositions.Count == 0) continue;
compositionMap[template.Id] = compositions;
@@ -96,7 +96,6 @@ public class DeploymentComparisonTests
templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any<CancellationToken>()).Returns(instance);
templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(template);
templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any<CancellationToken>()).Returns([]);
templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>()).Returns([]);
siteRepo.GetDataConnectionsBySiteIdAsync(SiteId, Arg.Any<CancellationToken>()).Returns([]);
@@ -59,7 +59,6 @@ public class FlattenSessionCacheTests
_templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(template);
_templateRepo.GetTemplateWithChildrenAsync(ParentTemplateId, Arg.Any<CancellationToken>()).Returns(parent);
_templateRepo.GetCompositionsByTemplateIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns([]);
for (var i = 1; i <= instanceCount; i++)
{
@@ -72,7 +72,6 @@ public class FlatteningPipelineConnectionBindingTests
_templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any<CancellationToken>()).Returns(instance);
_templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(template);
_templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any<CancellationToken>()).Returns([]);
_templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>()).Returns([]);
var connection = new DataConnection("PlantBus", "OpcUa", SiteId) { Id = ConnectionId };
@@ -64,8 +64,6 @@ public class FlatteningPipelineNativeAlarmCapabilityTests
_templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any<CancellationToken>()).Returns(instance);
_templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(template);
_templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any<CancellationToken>())
.Returns([]);
_templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>())
.Returns([]);
@@ -52,8 +52,8 @@ public class FlatteningPipelinePreviewTests
_templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(parent);
_templateRepo.GetTemplateWithChildrenAsync(ModuleTemplateId, Arg.Any<CancellationToken>()).Returns(module);
_templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any<CancellationToken>()).Returns([composition]);
_templateRepo.GetCompositionsByTemplateIdAsync(ModuleTemplateId, Arg.Any<CancellationToken>()).Returns([]);
// Compositions ride on the loaded template (GetTemplateWithChildrenAsync Includes them).
parent.Compositions.Add(composition);
_templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>()).Returns([]);
_templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any<CancellationToken>())
.Returns(new Instance("Line-01") { Id = InstanceId, TemplateId = TemplateId, SiteId = SiteId });
@@ -101,4 +101,21 @@ public class FlatteningPipelinePreviewTests
await _sut.PreviewAsync(TemplateId, SiteId);
await _templateRepo.DidNotReceive().GetInstanceByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>());
}
/// <summary>
/// review 2026-10-21 04/U5: the composition walk reads compositions off the already-loaded
/// template (GetTemplateWithChildrenAsync Includes them) and issues no separate
/// per-template composition query — while still resolving the composed module.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Flatten_reads_compositions_from_loaded_template_without_extra_query()
{
var result = await _sut.FlattenAndValidateAsync(InstanceId, validateScripts: false);
Assert.True(result.IsSuccess, result.IsSuccess ? "" : result.Error);
Assert.Contains(result.Value.Configuration.Attributes, a => a.CanonicalName == "Left.MoveInFlag");
await _templateRepo.DidNotReceive().GetCompositionsByTemplateIdAsync(
Arg.Any<int>(), Arg.Any<CancellationToken>());
}
}