feat(transport): normalize List attribute values to native JSON on import

This commit is contained in:
Joseph Doherty
2026-06-16 17:50:05 -04:00
parent 5841cec958
commit e3d804a1a6
4 changed files with 178 additions and 5 deletions
@@ -1040,7 +1040,7 @@ public sealed class BundleImporter : IBundleImporter
{
t.Attributes.Add(new TemplateAttribute(a.Name)
{
Value = a.Value,
Value = ImportValueNormalizer.NormalizeListValue(a.Value, a.DataType, a.ElementDataType),
DataType = a.DataType,
IsLocked = a.IsLocked,
Description = a.Description,
@@ -1115,11 +1115,17 @@ public sealed class BundleImporter : IBundleImporter
// Adds + Updates.
foreach (var attrDto in dto.Attributes)
{
// Normalise List values to the native-typed JSON form on import so the
// comparison (and the persisted value) match what the target already
// stores natively — otherwise an idempotent re-import of an old-form
// bundle would spuriously report a Value change.
var normalizedValue = ImportValueNormalizer.NormalizeListValue(
attrDto.Value, attrDto.DataType, attrDto.ElementDataType);
if (existingByName.TryGetValue(attrDto.Name, out var current))
{
// Update only if any field actually changed.
bool changed =
!string.Equals(current.Value, attrDto.Value, StringComparison.Ordinal) ||
!string.Equals(current.Value, normalizedValue, StringComparison.Ordinal) ||
current.DataType != attrDto.DataType ||
current.IsLocked != attrDto.IsLocked ||
!string.Equals(current.Description, attrDto.Description, StringComparison.Ordinal) ||
@@ -1127,7 +1133,7 @@ public sealed class BundleImporter : IBundleImporter
current.ElementDataType != attrDto.ElementDataType;
if (!changed) continue;
current.Value = attrDto.Value;
current.Value = normalizedValue;
current.DataType = attrDto.DataType;
current.IsLocked = attrDto.IsLocked;
current.Description = attrDto.Description;
@@ -1157,7 +1163,7 @@ public sealed class BundleImporter : IBundleImporter
{
var newAttr = new TemplateAttribute(attrDto.Name)
{
Value = attrDto.Value,
Value = normalizedValue,
DataType = attrDto.DataType,
IsLocked = attrDto.IsLocked,
Description = attrDto.Description,
@@ -199,7 +199,7 @@ public sealed class EntitySerializer
t.Attributes.Add(new TemplateAttribute(a.Name)
{
TemplateId = t.Id,
Value = a.Value,
Value = ImportValueNormalizer.NormalizeListValue(a.Value, a.DataType, a.ElementDataType),
DataType = a.DataType,
IsLocked = a.IsLocked,
Description = a.Description,
@@ -0,0 +1,51 @@
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
/// <summary>
/// Import-time normalization of attribute values to the native-typed JSON form.
/// <para>
/// Bundles exported before the native-typed-JSON change carry List attribute
/// values in the old quoted-element form (e.g. <c>["10","20"]</c> for an
/// <see cref="DataType.Int32"/> list). Already-exported bundle files can't be
/// rewritten, so List values are normalised on import: every DTO→entity write
/// site routes the value through <see cref="NormalizeListValue"/> so imported
/// data lands native (<c>[10,20]</c>). The central DB normalizer remains the
/// backstop for anything that slips through.
/// </para>
/// <para>
/// Non-List attributes and null/empty values pass through unchanged. A value
/// that fails to decode (malformed JSON / un-parseable element) is left exactly
/// as-is so the import still succeeds — the DB normalizer is the backstop.
/// </para>
/// </summary>
internal static class ImportValueNormalizer
{
/// <summary>
/// Returns the native-typed JSON form of a List attribute value, or the
/// value unchanged for non-List / null / empty / malformed inputs.
/// </summary>
/// <param name="value">The attribute value as carried by the bundle DTO.</param>
/// <param name="dataType">The attribute's declared data type.</param>
/// <param name="elementType">The List element type (null for scalars).</param>
public static string? NormalizeListValue(string? value, DataType dataType, DataType? elementType)
{
if (dataType != DataType.List || string.IsNullOrEmpty(value))
{
return value;
}
try
{
return AttributeValueCodec.Encode(
AttributeValueCodec.Decode(value, DataType.List, elementType));
}
catch (FormatException)
{
// Leave malformed values exactly as imported; the DB normalizer is
// the backstop. Never abort the import for a single bad value.
return value;
}
}
}