feat(commons): OpcUaEndpointConfigSerializer with legacy fallback + flat-dict interop

This commit is contained in:
Joseph Doherty
2026-05-12 00:44:21 -04:00
parent 90b252047e
commit 8fbf167389

View File

@@ -0,0 +1,119 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ScadaLink.Commons.Types.DataConnections;
namespace ScadaLink.Commons.Serialization;
/// <summary>
/// Serializes <see cref="OpcUaEndpointConfig"/> to/from the typed nested JSON
/// shape stored in <c>DataConnection.PrimaryConfiguration</c> / <c>BackupConfiguration</c>.
/// On read, falls back to the legacy flat string-dict shape for pre-refactor rows
/// and returns IsLegacy=true so the form can prompt the user to re-save.
/// </summary>
public static class OpcUaEndpointConfigSerializer
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
public static string Serialize(OpcUaEndpointConfig config)
=> JsonSerializer.Serialize(config, JsonOpts);
public static (OpcUaEndpointConfig Config, bool IsLegacy) Deserialize(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return (new OpcUaEndpointConfig(), false);
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind == JsonValueKind.Object
&& doc.RootElement.TryGetProperty("endpointUrl", out _))
{
var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
if (typed != null)
return (typed, false);
}
}
catch (JsonException) { /* fall through to legacy */ }
return (LoadLegacy(json!), IsLegacy: true);
}
/// <summary>
/// Flattens the typed config to the IDictionary&lt;string,string&gt; shape that
/// IDataConnection.ConnectAsync expects. Keys match the historical convention
/// used by OpcUaDataConnection so the adapter can keep that interface.
/// </summary>
public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config)
{
var dict = new Dictionary<string, string>
{
["endpoint"] = config.EndpointUrl,
["SecurityMode"] = config.SecurityMode.ToString(),
["AutoAcceptUntrustedCerts"] = config.AutoAcceptUntrustedCerts.ToString(),
["SessionTimeoutMs"] = config.SessionTimeoutMs.ToString(),
["OperationTimeoutMs"] = config.OperationTimeoutMs.ToString(),
["PublishingIntervalMs"] = config.PublishingIntervalMs.ToString(),
["SamplingIntervalMs"] = config.SamplingIntervalMs.ToString(),
["QueueSize"] = config.QueueSize.ToString(),
["KeepAliveCount"] = config.KeepAliveCount.ToString(),
["LifetimeCount"] = config.LifetimeCount.ToString(),
["MaxNotificationsPerPublish"] = config.MaxNotificationsPerPublish.ToString(),
};
if (config.Heartbeat is { } hb)
{
dict["HeartbeatTagPath"] = hb.TagPath;
dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString();
}
return dict;
}
public static OpcUaEndpointConfig FromFlatDict(IDictionary<string, string> dict)
{
var c = new OpcUaEndpointConfig
{
EndpointUrl = dict.TryGetValue("endpoint", out var ep) ? ep
: dict.TryGetValue("EndpointUrl", out var ep2) ? ep2 : "",
SecurityMode = Enum.TryParse<OpcUaSecurityMode>(
dict.TryGetValue("SecurityMode", out var smStr) ? smStr : null,
ignoreCase: true, out var sm)
? sm : OpcUaSecurityMode.None,
AutoAcceptUntrustedCerts = ParseBool(dict, "AutoAcceptUntrustedCerts", true),
SessionTimeoutMs = ParseInt(dict, "SessionTimeoutMs", 60000),
OperationTimeoutMs = ParseInt(dict, "OperationTimeoutMs", 15000),
PublishingIntervalMs = ParseInt(dict, "PublishingIntervalMs", 1000),
SamplingIntervalMs = ParseInt(dict, "SamplingIntervalMs", 1000),
QueueSize = ParseInt(dict, "QueueSize", 10),
KeepAliveCount = ParseInt(dict, "KeepAliveCount", 10),
LifetimeCount = ParseInt(dict, "LifetimeCount", 30),
MaxNotificationsPerPublish = ParseInt(dict, "MaxNotificationsPerPublish", 100),
};
if (dict.TryGetValue("HeartbeatTagPath", out var hbPath)
&& !string.IsNullOrWhiteSpace(hbPath))
{
c.Heartbeat = new OpcUaHeartbeatConfig
{
TagPath = hbPath,
MaxSilenceSeconds = ParseInt(dict, "HeartbeatMaxSilence", 30)
};
}
return c;
}
private static OpcUaEndpointConfig LoadLegacy(string json)
{
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json)
?? new Dictionary<string, string>();
return FromFlatDict(dict);
}
private static int ParseInt(IDictionary<string, string> d, string key, int defaultValue)
=> d.TryGetValue(key, out var s) && int.TryParse(s, out var v) ? v : defaultValue;
private static bool ParseBool(IDictionary<string, string> d, string key, bool defaultValue)
=> d.TryGetValue(key, out var s) && bool.TryParse(s, out var v) ? v : defaultValue;
}