feat(templates): GetTemplateStubsAsync untracked identity/edge projection

This commit is contained in:
Joseph Doherty
2026-10-22 06:06:27 -04:00
parent 15c9de0f4a
commit 6bcca4b344
4 changed files with 57 additions and 0 deletions
@@ -1,6 +1,7 @@
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
@@ -55,6 +56,12 @@ public interface ITemplateEngineRepository
/// <returns>A task that resolves to a read-only list of all templates without script bodies.</returns>
Task<IReadOnlyList<Template>> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Returns an untracked identity/edge projection of every template (no children), ordered by name.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of template stubs.</returns>
Task<IReadOnlyList<TemplateStub>> GetTemplateStubsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Database-side paged listing of templates as row summaries (child
/// collections reduced to counts, no script bodies). Backs the
/// <c>ListTemplates</c> management command, which previously materialised
@@ -0,0 +1,19 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
/// <summary>
/// Lightweight template identity/edge projection (no attributes, alarms, scripts or compositions)
/// for callers that only need names and the inheritance/folder/ownership edges.
/// </summary>
/// <param name="Id">Template ID.</param>
/// <param name="Name">Template name.</param>
/// <param name="ParentTemplateId">Base template, if any.</param>
/// <param name="FolderId">Containing folder, if any.</param>
/// <param name="IsDerived">True for auto-derived composition templates.</param>
/// <param name="OwnerCompositionId">Owning composition row for derived templates.</param>
public sealed record TemplateStub(
int Id,
string Name,
int? ParentTemplateId,
int? FolderId,
bool IsDerived,
int? OwnerCompositionId);
@@ -90,6 +90,16 @@ public class TemplateEngineRepository : ITemplateEngineRepository
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<TemplateStub>> GetTemplateStubsAsync(CancellationToken cancellationToken = default)
{
return await _context.Templates
.AsNoTracking()
.OrderBy(t => t.Name)
.Select(t => new TemplateStub(t.Id, t.Name, t.ParentTemplateId, t.FolderId, t.IsDerived, t.OwnerCompositionId))
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
// Two deliberate departures from <see cref="GetAllTemplatesAsync"/>:
// <list type="number">
@@ -31,6 +31,27 @@ public class TemplateEngineRepositoryTests : IDisposable
_context.Dispose();
}
/// <summary>Verifies the stub projection carries identity and edges and leaves nothing tracked.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetTemplateStubs_ProjectsEdgesUntrackedOrderedByName()
{
var baseT = new Template("Base");
_context.Templates.Add(baseT);
await _context.SaveChangesAsync();
_context.Templates.Add(new Template("Alpha") { ParentTemplateId = baseT.Id });
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var stubs = await _repository.GetTemplateStubsAsync();
Assert.Equal(new[] { "Alpha", "Base" }, stubs.Select(s => s.Name));
Assert.Equal(baseT.Id, stubs[0].ParentTemplateId);
Assert.Null(stubs[1].ParentTemplateId);
Assert.False(stubs[0].IsDerived);
Assert.Empty(_context.ChangeTracker.Entries());
}
/// <summary>Verifies that the topology page is bounded, filtered and returned untracked.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]