using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; /// /// Idempotent central startup normalizer that rewrites already-persisted List attribute /// values from the old array-of-strings JSON form (["10","20"]) to the new /// native-typed form ([10,20]). /// /// /// This is a safety net: at the time of writing there is no deployed List attribute data, /// so in practice it finds nothing to rewrite. It runs on every central startup after /// migrations apply and MUST never abort startup for data reasons — every row's work is /// wrapped in a per-row try/catch that logs and skips on malformed data. A second run /// finds nothing to change (native → native re-encode is byte-identical). /// /// public static class ListValueNormalizer { /// /// Rewrites old-form List attribute values to the native-typed JSON form across /// and /// . Idempotent and /// best-effort: malformed rows are logged and skipped, never rethrown. /// /// The configuration database context. /// Optional logger for diagnostics. /// Cancellation token. /// A task that represents the asynchronous operation. public static async Task NormalizeAsync( ScadaBridgeDbContext db, ILogger? logger = null, CancellationToken ct = default) { var rewritten = 0; // TemplateAttributes: List rows carry the element type on the row itself. var templateRows = await db.TemplateAttributes .Where(a => a.DataType == DataType.List) .ToListAsync(ct); foreach (var a in templateRows) { try { var native = AttributeValueCodec.Encode( AttributeValueCodec.Decode(a.Value, DataType.List, a.ElementDataType)); if (native != a.Value) { a.Value = native; rewritten++; } } catch (Exception ex) { // Never abort startup for a single bad row. FormatException from Decode is the // expected case; the broad catch also covers an unexpected serialize failure // (e.g. a JsonException on a non-finite value) so one poison row can't crash boot. logger?.LogWarning(ex, "List value normalizer: skipping unprocessable list value for TemplateAttribute {Id}.", a.Id); } } // InstanceAttributeOverrides: only rows that carry an element type are List rows. // Rows with a null ElementDataType are scalar/legacy rows (no deployed List data // exists, so none in practice) and are skipped. var overrideRows = await db.InstanceAttributeOverrides .Where(o => o.ElementDataType != null) .ToListAsync(ct); foreach (var o in overrideRows) { try { var native = AttributeValueCodec.Encode( AttributeValueCodec.Decode(o.OverrideValue, DataType.List, o.ElementDataType)); if (native != o.OverrideValue) { o.OverrideValue = native; rewritten++; } } catch (Exception ex) { logger?.LogWarning(ex, "List value normalizer: skipping unprocessable list value for InstanceAttributeOverride {Id}.", o.Id); } } try { await db.SaveChangesAsync(ct); } catch (Exception ex) { // A catastrophic DB failure on SaveChanges may propagate, but log it first so // startup diagnostics are not silent. Per-row data problems are already handled // above and never reach here. logger?.LogError(ex, "List value normalizer: SaveChanges failed."); throw; } if (rewritten > 0) { logger?.LogInformation( "List value normalizer: rewrote {n} attribute value(s) to native JSON.", rewritten); } else { logger?.LogDebug("List value normalizer: no attribute values required rewriting."); } } }