diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/AttributeValueCodec.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/AttributeValueCodec.cs index ecc4cbea..b848b79e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/AttributeValueCodec.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/AttributeValueCodec.cs @@ -25,11 +25,10 @@ public static class AttributeValueCodec case string s: return s; // already canonical case IFormattable f: return f.ToString(null, CultureInfo.InvariantCulture); case IEnumerable e: - var items = e.Cast() - .Select(x => x is IFormattable xf - ? xf.ToString(null, CultureInfo.InvariantCulture) - : x?.ToString()); - return JsonSerializer.Serialize(items, JsonOpts); + // Native-typed JSON: serialize the runtime collection so System.Text.Json emits + // numbers/bools unquoted, strings quoted, and DateTime as ISO-8601. Boxed as object + // so STJ uses the runtime element type. STJ numbers/dates are culture-invariant. + return JsonSerializer.Serialize(e, JsonOpts); default: return value.ToString(); } } @@ -46,18 +45,25 @@ public static class AttributeValueCodec if (elementType is null) throw new FormatException("List attribute requires an element type."); - string?[] raw; - try { raw = JsonSerializer.Deserialize(value) ?? []; } + JsonElement[] raw; + try { raw = JsonSerializer.Deserialize(value) ?? []; } catch (JsonException ex) { throw new FormatException("Malformed list JSON.", ex); } var clrType = ElementClrType(elementType.Value); var listType = typeof(List<>).MakeGenericType(clrType); var result = (IList)Activator.CreateInstance(listType)!; - foreach (var item in raw) - result.Add(ParseScalar(item, elementType.Value)); + foreach (var el in raw) + result.Add(ParseScalar(JsonElementToString(el), elementType.Value)); return result; } + private static string? JsonElementToString(JsonElement el) => el.ValueKind switch + { + JsonValueKind.String => el.GetString(), // old form, or string-typed lists + JsonValueKind.Null => null, // ParseScalar throws "may not be null" + _ => el.GetRawText() // number/bool → "10" / "1.5" / "true" + }; + private static Type ElementClrType(DataType t) => t switch { DataType.String => typeof(string), diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/AttributeValueCodecTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/AttributeValueCodecTests.cs index a4aed551..15229611 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/AttributeValueCodecTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/AttributeValueCodecTests.cs @@ -64,7 +64,7 @@ public class AttributeValueCodecTests try { CultureInfo.CurrentCulture = new CultureInfo("de-DE"); - Assert.Equal("[\"1.5\",\"2.5\"]", + Assert.Equal("[1.5,2.5]", AttributeValueCodec.Encode(new List { 1.5, 2.5 })); } finally @@ -73,6 +73,53 @@ public class AttributeValueCodecTests } } + [Fact] + public void Encode_Int32List_ProducesNativeNumbers() => + Assert.Equal("[10,20,30]", + AttributeValueCodec.Encode(new List { 10, 20, 30 })); + + [Fact] + public void Encode_BoolList_ProducesNativeBooleans() => + Assert.Equal("[true,false]", + AttributeValueCodec.Encode(new List { true, false })); + + [Fact] + public void Encode_StringList_StaysQuoted() => + Assert.Equal("[\"a\",\"b\"]", + AttributeValueCodec.Encode(new List { "a", "b" })); + + [Fact] + public void Encode_DateTimeList_IsIso8601() + { + var json = AttributeValueCodec.Encode( + new List { new(2026, 6, 16, 0, 0, 0, DateTimeKind.Utc) }); + Assert.Contains("2026-06-16T00:00:00", json); + Assert.DoesNotContain("06/16/2026", json); + } + + [Fact] + public void Decode_NewNativeIntForm_Parses() + { + var back = (IList)AttributeValueCodec.Decode("[10,20]", DataType.List, DataType.Int32)!; + Assert.Equal(new[] { 10, 20 }, back); + } + + [Fact] + public void Decode_OldStringIntForm_BackwardCompatible() + { + var back = (IList)AttributeValueCodec.Decode("[\"10\",\"20\"]", DataType.List, DataType.Int32)!; + Assert.Equal(new[] { 10, 20 }, back); + } + + [Theory] + [InlineData("[true,false]")] + [InlineData("[\"True\",\"False\"]")] + public void Decode_BoolForms_BothParse(string json) + { + var back = (IList)AttributeValueCodec.Decode(json, DataType.List, DataType.Boolean)!; + Assert.Equal(new[] { true, false }, back); + } + [Fact] public void RoundTrip_Int32List() {