feat(templateengine): ResolvedAttribute.DefaultDataSourceReference — pre-binding default for the Configure page; excluded from the revision hash (pinned)
This commit is contained in:
@@ -78,6 +78,17 @@ public sealed record ResolvedAttribute
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DataSourceReference { get; init; }
|
public string? DataSourceReference { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data-source reference as resolved from the template chain BEFORE any
|
||||||
|
/// per-instance <c>InstanceConnectionBinding.DataSourceReferenceOverride</c> was
|
||||||
|
/// applied — i.e. the template/composed default the Configure page shows as the
|
||||||
|
/// placeholder. Equals <see cref="DataSourceReference"/> when no override is set;
|
||||||
|
/// null for attributes that are not data-sourced.
|
||||||
|
/// Central-side presentation data only: deliberately excluded from the revision
|
||||||
|
/// hash (<c>RevisionHashService</c> projects through <c>HashableAttribute</c>).
|
||||||
|
/// </summary>
|
||||||
|
public string? DefaultDataSourceReference { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If data-sourced: the resolved connection ID from the instance binding.
|
/// If data-sourced: the resolved connection ID from the instance binding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -369,6 +369,15 @@ public class FlatteningService
|
|||||||
Dictionary<string, ResolvedAttribute> attributes,
|
Dictionary<string, ResolvedAttribute> attributes,
|
||||||
IReadOnlyDictionary<int, DataConnection> dataConnections)
|
IReadOnlyDictionary<int, DataConnection> dataConnections)
|
||||||
{
|
{
|
||||||
|
// Capture the pre-binding default for EVERY data-sourced attribute (bound or
|
||||||
|
// not) so the Configure page can show the template default as a placeholder.
|
||||||
|
foreach (var key in attributes.Keys.ToList())
|
||||||
|
{
|
||||||
|
var attr = attributes[key];
|
||||||
|
if (attr.DataSourceReference is not null && attr.DefaultDataSourceReference is null)
|
||||||
|
attributes[key] = attr with { DefaultDataSourceReference = attr.DataSourceReference };
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var binding in bindings)
|
foreach (var binding in bindings)
|
||||||
{
|
{
|
||||||
if (!attributes.TryGetValue(binding.AttributeName, out var existing))
|
if (!attributes.TryGetValue(binding.AttributeName, out var existing))
|
||||||
@@ -385,7 +394,8 @@ public class FlatteningService
|
|||||||
BoundDataConnectionId = connection.Id,
|
BoundDataConnectionId = connection.Id,
|
||||||
BoundDataConnectionName = connection.Name,
|
BoundDataConnectionName = connection.Name,
|
||||||
BoundDataConnectionProtocol = connection.Protocol,
|
BoundDataConnectionProtocol = connection.Protocol,
|
||||||
DataSourceReference = binding.DataSourceReferenceOverride ?? existing.DataSourceReference
|
DataSourceReference = binding.DataSourceReferenceOverride ?? existing.DataSourceReference,
|
||||||
|
DefaultDataSourceReference = existing.DefaultDataSourceReference ?? existing.DataSourceReference
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Flattening;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Design 2026-08-20 (grouped connection bindings): the flattened attribute
|
||||||
|
/// carries the data-source reference as it was BEFORE the instance binding
|
||||||
|
/// override, so the Configure page can show the template default next to the
|
||||||
|
/// effective reference. The field is deliberately NOT part of the revision hash.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultDataSourceReferenceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RevisionHash_IgnoresDefaultDataSourceReference()
|
||||||
|
{
|
||||||
|
var svc = new RevisionHashService();
|
||||||
|
var a = new ResolvedAttribute
|
||||||
|
{
|
||||||
|
CanonicalName = "LeftMESReceiver.MoveInFlag",
|
||||||
|
DataType = "Boolean",
|
||||||
|
DataSourceReference = "MESReceiver_022.MoveInFlag",
|
||||||
|
BoundDataConnectionId = 1
|
||||||
|
};
|
||||||
|
var without = new FlattenedConfiguration { InstanceUniqueName = "Z28061", Attributes = [a] };
|
||||||
|
var with = without with { Attributes = [a with { DefaultDataSourceReference = "MoveInFlag" }] };
|
||||||
|
|
||||||
|
Assert.Equal(svc.ComputeHash(without), svc.ComputeHash(with));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolvedAttribute_DefaultDataSourceReference_IsNullByDefault()
|
||||||
|
{
|
||||||
|
var a = new ResolvedAttribute { CanonicalName = "X" };
|
||||||
|
Assert.Null(a.DefaultDataSourceReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Flatten_PopulatesDefault_ForBoundAndUnboundDataSourcedAttributes()
|
||||||
|
{
|
||||||
|
var template = new Template("Base") { Id = 1 };
|
||||||
|
template.Attributes.Add(new TemplateAttribute("MoveInFlag") { DataType = DataType.Boolean, DataSourceReference = "MoveInFlag" });
|
||||||
|
template.Attributes.Add(new TemplateAttribute("Unbound") { DataType = DataType.Boolean, DataSourceReference = "UnboundRef" });
|
||||||
|
template.Attributes.Add(new TemplateAttribute("Static") { DataType = DataType.Boolean, Value = "false" });
|
||||||
|
|
||||||
|
var instance = new Instance("Z28061") { Id = 1, TemplateId = 1, SiteId = 1 };
|
||||||
|
instance.ConnectionBindings.Add(new InstanceConnectionBinding("MoveInFlag")
|
||||||
|
{
|
||||||
|
DataConnectionId = 1,
|
||||||
|
DataSourceReferenceOverride = "MESReceiver_022.MoveInFlag"
|
||||||
|
});
|
||||||
|
|
||||||
|
var connections = new Dictionary<int, DataConnection>
|
||||||
|
{
|
||||||
|
[1] = new("GW", "MxGateway", 1) { Id = 1, PrimaryConfiguration = "http://localhost" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new FlatteningService().Flatten(
|
||||||
|
instance,
|
||||||
|
[template],
|
||||||
|
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||||||
|
new Dictionary<int, IReadOnlyList<Template>>(),
|
||||||
|
connections);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
|
||||||
|
var bound = result.Value.Attributes.Single(a => a.CanonicalName == "MoveInFlag");
|
||||||
|
Assert.Equal("MESReceiver_022.MoveInFlag", bound.DataSourceReference);
|
||||||
|
Assert.Equal("MoveInFlag", bound.DefaultDataSourceReference);
|
||||||
|
|
||||||
|
var unbound = result.Value.Attributes.Single(a => a.CanonicalName == "Unbound");
|
||||||
|
Assert.Equal("UnboundRef", unbound.DataSourceReference);
|
||||||
|
Assert.Equal("UnboundRef", unbound.DefaultDataSourceReference);
|
||||||
|
|
||||||
|
var stat = result.Value.Attributes.Single(a => a.CanonicalName == "Static");
|
||||||
|
Assert.Null(stat.DefaultDataSourceReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user