diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs
index 1182af78..d17bcfe5 100644
--- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs
@@ -119,6 +119,11 @@ public class DiffService
a.CanonicalName == b.CanonicalName &&
a.Value == b.Value &&
a.DataType == b.DataType &&
+ // #290: ElementDataType is part of the revision hash, so the diff must
+ // treat a List element-type change (e.g. Int32 → Double) with identical
+ // JSON-encoded values as a Changed attribute — keeping the structured
+ // diff in parity with the staleness hash.
+ a.ElementDataType == b.ElementDataType &&
a.Description == b.Description &&
a.IsLocked == b.IsLocked &&
a.DataSourceReference == b.DataSourceReference &&
diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs
index 19337841..8bdc978d 100644
--- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs
@@ -53,6 +53,7 @@ public class RevisionHashService
CanonicalName = a.CanonicalName,
Value = a.Value,
DataType = a.DataType,
+ ElementDataType = a.ElementDataType,
Description = a.Description,
IsLocked = a.IsLocked,
DataSourceReference = a.DataSourceReference,
@@ -174,6 +175,16 @@ public class RevisionHashService
///
public string? Description { get; init; }
///
+ /// For List attributes: the element scalar type name; null otherwise.
+ /// Folded into the hash (#290) so a List element-type change (e.g.
+ /// Int32 → Double) with identical JSON-encoded values is detected as a
+ /// staleness/revision change. Null for scalar attributes — and because
+ /// the serializer ignores null properties
+ /// (), scalar-only
+ /// configurations hash identically to before this field was added.
+ ///
+ public string? ElementDataType { get; init; }
+ ///
/// Whether the attribute is locked.
///
public bool IsLocked { get; init; }
diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs
index fb5fa89f..ef83f798 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs
@@ -162,6 +162,37 @@ public class DiffServiceTests
Assert.Equal(DiffChangeType.Changed, diff.AttributeChanges[0].ChangeType);
}
+ [Fact]
+ public void ComputeDiff_AttributeElementDataTypeChange_DetectedAsChanged()
+ {
+ // #290: ElementDataType is part of the revision hash, so AttributesEqual
+ // must also compare it — a List element-type change (Int32 → Double) with
+ // identical JSON-encoded values must surface as a Changed attribute,
+ // keeping the structured diff in parity with the staleness hash.
+ var oldConfig = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ Attributes =
+ [
+ new ResolvedAttribute { CanonicalName = "Readings", Value = "[1,2]", DataType = "List", ElementDataType = "Int32" }
+ ]
+ };
+ var newConfig = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ Attributes =
+ [
+ new ResolvedAttribute { CanonicalName = "Readings", Value = "[1,2]", DataType = "List", ElementDataType = "Double" }
+ ]
+ };
+
+ var diff = _sut.ComputeDiff(oldConfig, newConfig);
+
+ Assert.True(diff.HasChanges);
+ Assert.Single(diff.AttributeChanges);
+ Assert.Equal(DiffChangeType.Changed, diff.AttributeChanges[0].ChangeType);
+ }
+
[Fact]
public void ComputeDiff_AlarmDescriptionChange_DetectedAsChanged()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs
index 0c13f8ff..93fd1323 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/RevisionHashServiceTests.cs
@@ -279,6 +279,94 @@ public class RevisionHashServiceTests
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
}
+ [Fact]
+ public void ComputeHash_ListElementDataTypeChange_ChangesHash()
+ {
+ // #290: a List attribute whose element type changes (Int32 → Double)
+ // while the JSON-encoded values stay identical MUST produce a different
+ // revision hash, so the deployment is flagged stale. ElementDataType is
+ // folded into the hashable projection precisely for this case.
+ var baseAttr = new ResolvedAttribute
+ {
+ CanonicalName = "Readings",
+ Value = "[1,2]",
+ DataType = "List",
+ ElementDataType = "Int32"
+ };
+ var editedAttr = baseAttr with { ElementDataType = "Double" };
+
+ var configBefore = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ TemplateId = 1,
+ SiteId = 1,
+ Attributes = [baseAttr]
+ };
+ var configAfter = configBefore with { Attributes = [editedAttr] };
+
+ Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
+ }
+
+ [Fact]
+ public void ComputeHash_SameListElementDataType_SameHash()
+ {
+ // #290: two List attributes identical in every field — including
+ // ElementDataType — must hash the same.
+ var attr = new ResolvedAttribute
+ {
+ CanonicalName = "Readings",
+ Value = "[1,2]",
+ DataType = "List",
+ ElementDataType = "Int32"
+ };
+
+ var config1 = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ TemplateId = 1,
+ SiteId = 1,
+ Attributes = [attr]
+ };
+ var config2 = config1 with { Attributes = [attr with { }] };
+
+ Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
+ }
+
+ [Fact]
+ public void ComputeHash_ScalarAttribute_NullVsUnsetElementDataType_SameHash()
+ {
+ // #290 no-op guard: a scalar attribute has a null ElementDataType, and
+ // the canonical serializer omits null properties
+ // (DefaultIgnoreCondition = WhenWritingNull). Explicitly setting
+ // ElementDataType = null must therefore hash identically to leaving it
+ // at its default — i.e. adding the field is a no-op for scalars and does
+ // not retroactively change their hashes.
+ var withExplicitNull = new ResolvedAttribute
+ {
+ CanonicalName = "Temperature",
+ Value = "25",
+ DataType = "Double",
+ ElementDataType = null
+ };
+ var withDefault = new ResolvedAttribute
+ {
+ CanonicalName = "Temperature",
+ Value = "25",
+ DataType = "Double"
+ };
+
+ var config1 = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Instance1",
+ TemplateId = 1,
+ SiteId = 1,
+ Attributes = [withExplicitNull]
+ };
+ var config2 = config1 with { Attributes = [withDefault] };
+
+ Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
+ }
+
private static FlattenedConfiguration CreateConfig(string instanceName, string tempValue)
{
return new FlattenedConfiguration