Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandDtoMapper.cs
T
Joseph Doherty dd4f1c2aa3 fix(alarms): Severity runs one direction, higher = more urgent; raw priority kept (#129)
AlarmConditionState.Severity mixed two conventions: OPC UA rows carried Part 9
Severity (higher = more severe) while MxGateway native alarms and computed
alarms carried a raw AVEVA-style priority (1 = most urgent). Every consumer
ranks higher as worse, so Worst Severity, the severity filter/sort and the
site event-log Error threshold picked the LEAST urgent MxGateway/computed
alarms.

- AlarmPriorityBands: the OtOpcUa GalaxyAlarmPriority table (lmxopcua#887,
  b61f3e05), value for value: 1-250 -> 1000, 251-500 -> 750, 501-750 -> 500,
  751-999 -> 250; <= 0 clamps to Critical, > 999 to Low.
- MxGatewayAlarmMapper bands the gateway severity (= AVEVA priority);
  AlarmConditionStateFactory.ForComputed bands PriorityLevel. OPC UA unchanged.
- AlarmConditionState gains trailing Priority (int?): the raw source number,
  exactly the pre-#129 Severity. AlarmStateChanged.Priority and the wire
  AlarmStateUpdate.priority keep that meaning (no proto change); central derives
  Severity by kind (SeverityFromPriority), so an older site decodes correctly.
- ScriptAlarm gains trailing Priority. The native MES 900-999 band and the MES
  API Severity/MinSeverity/MaxSeverity key off Priority; rig seed scripts moved
  from a.Severity to a.Priority, so MES output is unchanged.
- AlarmActor raise logs Error when the banded severity >= 700 (same threshold
  as NativeAlarmActor): priority 1-500 Error, 501+ Warning (was raw >= 700).
- NativeAlarmActor upgrades pre-#129 native_alarm_state rows on rehydrate.
- Debug snapshot decode restores Condition.Priority from the row's priority.
- Docs: Commons, DCL, SiteRuntime, Communication, CentralUI, InboundAPI,
  HighLevelReqs, MES alarm-status plan, CLAUDE.md.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 14:02:54 -05:00

3147 lines
135 KiB
C#

using Google.Protobuf;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using BoolValue = Google.Protobuf.WellKnownTypes.BoolValue;
using Duration = Google.Protobuf.WellKnownTypes.Duration;
using Int32Value = Google.Protobuf.WellKnownTypes.Int32Value;
using StringValue = Google.Protobuf.WellKnownTypes.StringValue;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Canonical bridge between the in-process central→site command/reply records
/// (Commons <c>Messages.*</c>) and the wire messages of
/// <c>Protos/site_command.proto</c>. Every one of the 28 migrated commands and
/// all 22 reply shapes pass through here; nothing else may hand-roll the
/// translation, so the contract has exactly one definition.
/// </summary>
/// <remarks>
/// <para>
/// Mirrors the sibling <see cref="AuditEventDtoMapper"/>/<see cref="SiteCallDtoMapper"/>
/// and lives in <c>Communication</c> for the same reason: this project owns the
/// generated wire types and already references <c>Commons</c>, so both the site
/// server (T1B.2) and the central transport (T1B.3) can share one implementation
/// without a project-reference cycle.
/// </para>
/// <para><b>Null conventions.</b> proto3 has no presence for scalars, so:</para>
/// <list type="bullet">
/// <item>
/// Nullable <b>strings</b> ride as plain <c>string</c> and the empty string
/// represents <c>null</c> — the convention <see cref="AuditEventDtoMapper"/>
/// already established. It is a normalisation, not a loss: every such member
/// here is an optional error message, token, filter or id where "" and
/// <c>null</c> carry the same meaning. The one exception is
/// <see cref="RouteToWaitForAttributeRequest.TargetValueEncoded"/>, where
/// "wait for the empty string" is a real target distinct from "no target";
/// that field uses a <c>StringValue</c> wrapper instead.
/// </item>
/// <item>
/// Nullable <b>ints/bools</b> use the <c>Int32Value</c>/<c>BoolValue</c>
/// wrappers, and nullable <b>enums</b> use a one-field message of the same
/// shape (proto3 enums have no presence and no stock wrapper exists).
/// </item>
/// <item>
/// Nullable <b>collections</b> are wrapped in a per-collection message, so
/// the null/empty distinction becomes a message-presence question. proto3
/// <c>repeated</c>/<c>map</c> alone collapses the two — precisely the silent
/// data-loss class the transport round-trip guard caught in the transport round-trip tests.
/// </item>
/// <item>
/// <c>Guid?</c> members ride as a <c>string</c> holding the "D" form, with
/// the empty string representing <c>null</c>.
/// <see cref="TrackedOperationId"/> (a non-nullable struct over a
/// <see cref="Guid"/>) rides as the same "D" string.
/// </item>
/// </list>
/// <para>
/// <b>Time.</b> DTO members declared <see cref="DateTimeOffset"/> use
/// <c>google.protobuf.Timestamp</c>. That normalises to UTC, which is lossless
/// under <see cref="DateTimeOffset"/> equality (it compares instants), and every
/// timestamp in this contract is documented UTC anyway. Members declared
/// <see cref="DateTime"/> (the certificate validity bounds) are coerced to
/// <see cref="DateTimeKind.Utc"/> first, matching their <c>…Utc</c> names.
/// Loosely-typed <c>object?</c> values are the exception and keep their exact
/// kind/offset — see <see cref="LooseValueCodec"/>.
/// </para>
/// <para>
/// <b>Enums</b> translate through explicit switches in both directions, never by
/// ordinal, so reordering a C# enum cannot silently re-map the wire. Every wire
/// enum reserves 0 for <c>_UNSPECIFIED</c>; decoding an unspecified or unknown
/// value yields the documented safe default for that enum (never a throw — a
/// version-skewed peer must not be able to fault a whole command).
/// </para>
/// </remarks>
public static class SiteCommandDtoMapper
{
// ─────────────────────────────────────────────────────────────────────
// Group classification — the authoritative 28-command inventory in code.
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Classifies a central→site command onto the RPC group that carries it.
/// The central transport (T1B.3) uses this to pick both the RPC and the
/// deadline; the coverage tests use it as the command inventory.
/// </summary>
/// <param name="command">The command record to classify.</param>
/// <returns>The <see cref="SiteCommandGroup"/> whose RPC carries the command.</returns>
/// <exception cref="ArgumentException">The type is not part of the migrated command set.</exception>
public static SiteCommandGroup GroupOf(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
RefreshDeploymentCommand or EnableInstanceCommand or DisableInstanceCommand
or DeleteInstanceCommand or DeploymentStateQueryRequest or DeployArtifactsCommand
=> SiteCommandGroup.Lifecycle,
BrowseNodeCommand or SearchAddressSpaceCommand or ReadTagValuesCommand
or VerifyEndpointCommand or TrustServerCertCommand or ListServerCertsCommand
or RemoveServerCertCommand or WriteTagRequest
=> SiteCommandGroup.OpcUa,
EventLogQueryRequest or DebugSnapshotRequest or SubscribeDebugViewRequest
or UnsubscribeDebugViewRequest
=> SiteCommandGroup.Query,
ParkedMessageQueryRequest or ParkedMessageRetryRequest or ParkedMessageDiscardRequest
or RetryParkedOperation or DiscardParkedOperation
=> SiteCommandGroup.Parked,
RouteToCallRequest or RouteToGetAttributesRequest or RouteToSetAttributesRequest
or RouteToWaitForAttributeRequest
=> SiteCommandGroup.Route,
TriggerSiteFailover => SiteCommandGroup.Failover,
_ => throw new ArgumentException(
$"'{command.GetType().Name}' is not a migrated site command.", nameof(command))
};
}
/// <summary>
/// Classifies a site→central reply onto the RPC group whose reply envelope
/// carries it — the mirror of <see cref="GroupOf"/>, used by the site
/// dispatcher to pack whatever a handler answered into the right envelope.
/// </summary>
/// <param name="reply">The reply record to classify.</param>
/// <returns>The <see cref="SiteCommandGroup"/> whose reply envelope carries it.</returns>
/// <exception cref="ArgumentException">The type is not a migrated reply shape.</exception>
public static SiteCommandGroup GroupOfReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
DeploymentStatusResponse or InstanceLifecycleResponse or DeploymentStateQueryResponse
or ArtifactDeploymentResponse
=> SiteCommandGroup.Lifecycle,
BrowseNodeResult or SearchAddressSpaceResult or ReadTagValuesResult or VerifyEndpointResult
or CertTrustResult or WriteTagResponse
=> SiteCommandGroup.OpcUa,
EventLogQueryResponse or DebugViewSnapshot or UnsubscribeDebugViewAck
=> SiteCommandGroup.Query,
ParkedMessageQueryResponse or ParkedMessageRetryResponse or ParkedMessageDiscardResponse
or ParkedOperationActionAck
=> SiteCommandGroup.Parked,
RouteToCallResponse or RouteToGetAttributesResponse or RouteToSetAttributesResponse
or RouteToWaitForAttributeResponse
=> SiteCommandGroup.Route,
SiteFailoverAck => SiteCommandGroup.Failover,
_ => throw new ArgumentException(
$"'{reply.GetType().Name}' is not a migrated site reply.", nameof(reply))
};
}
// ─────────────────────────────────────────────────────────────────────
// RPC 1 — ExecuteLifecycle: envelopes
// ─────────────────────────────────────────────────────────────────────
/// <summary>Packs a lifecycle-group command into its request envelope.</summary>
/// <param name="command">One of the six lifecycle-group commands.</param>
/// <returns>The populated request envelope.</returns>
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
public static LifecycleRequest ToLifecycleRequest(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
RefreshDeploymentCommand c => new LifecycleRequest { RefreshDeployment = ToProto(c) },
EnableInstanceCommand c => new LifecycleRequest { EnableInstance = ToProto(c) },
DisableInstanceCommand c => new LifecycleRequest { DisableInstance = ToProto(c) },
DeleteInstanceCommand c => new LifecycleRequest { DeleteInstance = ToProto(c) },
DeploymentStateQueryRequest c => new LifecycleRequest { DeploymentStateQuery = ToProto(c) },
DeployArtifactsCommand c => new LifecycleRequest { DeployArtifacts = ToProto(c) },
_ => throw NotInGroup(command, SiteCommandGroup.Lifecycle)
};
}
/// <summary>Unpacks a lifecycle-group request envelope back to its command record.</summary>
/// <param name="request">The request envelope received on the wire.</param>
/// <returns>The decoded command record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromLifecycleRequest(LifecycleRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return request.CommandCase switch
{
LifecycleRequest.CommandOneofCase.RefreshDeployment => FromProto(request.RefreshDeployment),
LifecycleRequest.CommandOneofCase.EnableInstance => FromProto(request.EnableInstance),
LifecycleRequest.CommandOneofCase.DisableInstance => FromProto(request.DisableInstance),
LifecycleRequest.CommandOneofCase.DeleteInstance => FromProto(request.DeleteInstance),
LifecycleRequest.CommandOneofCase.DeploymentStateQuery => FromProto(request.DeploymentStateQuery),
LifecycleRequest.CommandOneofCase.DeployArtifacts => FromProto(request.DeployArtifacts),
_ => throw UnknownCase(request.CommandCase, nameof(LifecycleRequest))
};
}
/// <summary>Packs a lifecycle-group reply into its reply envelope.</summary>
/// <param name="reply">One of the four lifecycle-group reply records.</param>
/// <returns>The populated reply envelope.</returns>
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
public static LifecycleReply ToLifecycleReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
DeploymentStatusResponse r => new LifecycleReply { DeploymentStatus = ToProto(r) },
InstanceLifecycleResponse r => new LifecycleReply { InstanceLifecycle = ToProto(r) },
DeploymentStateQueryResponse r => new LifecycleReply { DeploymentStateQuery = ToProto(r) },
ArtifactDeploymentResponse r => new LifecycleReply { ArtifactDeployment = ToProto(r) },
_ => throw NotInGroup(reply, SiteCommandGroup.Lifecycle)
};
}
/// <summary>Unpacks a lifecycle-group reply envelope back to its reply record.</summary>
/// <param name="reply">The reply envelope received on the wire.</param>
/// <returns>The decoded reply record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromLifecycleReply(LifecycleReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply.ReplyCase switch
{
LifecycleReply.ReplyOneofCase.DeploymentStatus => FromProto(reply.DeploymentStatus),
LifecycleReply.ReplyOneofCase.InstanceLifecycle => FromProto(reply.InstanceLifecycle),
LifecycleReply.ReplyOneofCase.DeploymentStateQuery => FromProto(reply.DeploymentStateQuery),
LifecycleReply.ReplyOneofCase.ArtifactDeployment => FromProto(reply.ArtifactDeployment),
_ => throw UnknownCase(reply.ReplyCase, nameof(LifecycleReply))
};
}
// ── Lifecycle commands ──
/// <summary>Projects a <see cref="RefreshDeploymentCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static RefreshDeploymentCommandDto ToProto(RefreshDeploymentCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new RefreshDeploymentCommandDto
{
DeploymentId = command.DeploymentId,
InstanceUniqueName = command.InstanceUniqueName,
RevisionHash = command.RevisionHash,
DeployedBy = command.DeployedBy,
Timestamp = Ts(command.Timestamp),
CentralFetchBaseUrl = command.CentralFetchBaseUrl,
FetchToken = command.FetchToken
};
}
/// <summary>Rehydrates a <see cref="RefreshDeploymentCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static RefreshDeploymentCommand FromProto(RefreshDeploymentCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RefreshDeploymentCommand(
dto.DeploymentId,
dto.InstanceUniqueName,
dto.RevisionHash,
dto.DeployedBy,
Dto(dto.Timestamp),
dto.CentralFetchBaseUrl,
dto.FetchToken);
}
/// <summary>Projects an <see cref="EnableInstanceCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static EnableInstanceCommandDto ToProto(EnableInstanceCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new EnableInstanceCommandDto
{
CommandId = command.CommandId,
InstanceUniqueName = command.InstanceUniqueName,
Timestamp = Ts(command.Timestamp)
};
}
/// <summary>Rehydrates an <see cref="EnableInstanceCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static EnableInstanceCommand FromProto(EnableInstanceCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new EnableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DisableInstanceCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static DisableInstanceCommandDto ToProto(DisableInstanceCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new DisableInstanceCommandDto
{
CommandId = command.CommandId,
InstanceUniqueName = command.InstanceUniqueName,
Timestamp = Ts(command.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="DisableInstanceCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static DisableInstanceCommand FromProto(DisableInstanceCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DisableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DeleteInstanceCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static DeleteInstanceCommandDto ToProto(DeleteInstanceCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new DeleteInstanceCommandDto
{
CommandId = command.CommandId,
InstanceUniqueName = command.InstanceUniqueName,
Timestamp = Ts(command.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="DeleteInstanceCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static DeleteInstanceCommand FromProto(DeleteInstanceCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DeleteInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DeploymentStateQueryRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static DeploymentStateQueryRequestDto ToProto(DeploymentStateQueryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new DeploymentStateQueryRequestDto
{
CorrelationId = request.CorrelationId,
InstanceUniqueName = request.InstanceUniqueName,
Timestamp = Ts(request.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="DeploymentStateQueryRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static DeploymentStateQueryRequest FromProto(DeploymentStateQueryRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DeploymentStateQueryRequest(dto.CorrelationId, dto.InstanceUniqueName, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DeployArtifactsCommand"/> onto the wire.</summary>
/// <remarks>Each artifact collection is nullable; a null collection leaves its wrapper message unset.</remarks>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static DeployArtifactsCommandDto ToProto(DeployArtifactsCommand command)
{
ArgumentNullException.ThrowIfNull(command);
var dto = new DeployArtifactsCommandDto
{
DeploymentId = command.DeploymentId,
Timestamp = Ts(command.Timestamp)
};
if (command.SharedScripts is not null)
{
var list = new SharedScriptArtifactListDto();
list.Items.AddRange(command.SharedScripts.Select(ToProto));
dto.SharedScripts = list;
}
if (command.ExternalSystems is not null)
{
var list = new ExternalSystemArtifactListDto();
list.Items.AddRange(command.ExternalSystems.Select(ToProto));
dto.ExternalSystems = list;
}
if (command.DatabaseConnections is not null)
{
var list = new DatabaseConnectionArtifactListDto();
list.Items.AddRange(command.DatabaseConnections.Select(ToProto));
dto.DatabaseConnections = list;
}
if (command.NotificationLists is not null)
{
var list = new NotificationListArtifactListDto();
list.Items.AddRange(command.NotificationLists.Select(ToProto));
dto.NotificationLists = list;
}
if (command.DataConnections is not null)
{
var list = new DataConnectionArtifactListDto();
list.Items.AddRange(command.DataConnections.Select(ToProto));
dto.DataConnections = list;
}
if (command.SmtpConfigurations is not null)
{
var list = new SmtpConfigurationArtifactListDto();
list.Items.AddRange(command.SmtpConfigurations.Select(ToProto));
dto.SmtpConfigurations = list;
}
if (command.Secrets is not null)
{
var list = new SecretArtifactListDto();
list.Items.AddRange(command.Secrets.Select(ToProto));
dto.Secrets = list;
}
return dto;
}
/// <summary>Rehydrates a <see cref="DeployArtifactsCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record; unset collection wrappers rehydrate as null.</returns>
public static DeployArtifactsCommand FromProto(DeployArtifactsCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DeployArtifactsCommand(
dto.DeploymentId,
dto.SharedScripts is null ? null : dto.SharedScripts.Items.Select(FromProto).ToList(),
dto.ExternalSystems is null ? null : dto.ExternalSystems.Items.Select(FromProto).ToList(),
dto.DatabaseConnections is null ? null : dto.DatabaseConnections.Items.Select(FromProto).ToList(),
dto.NotificationLists is null ? null : dto.NotificationLists.Items.Select(FromProto).ToList(),
dto.DataConnections is null ? null : dto.DataConnections.Items.Select(FromProto).ToList(),
dto.SmtpConfigurations is null ? null : dto.SmtpConfigurations.Items.Select(FromProto).ToList(),
Dto(dto.Timestamp),
dto.Secrets is null ? null : dto.Secrets.Items.Select(FromProto).ToList());
}
/// <summary>Projects a <see cref="SecretArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static SecretArtifactDto ToProto(SecretArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new SecretArtifactDto
{
Name = artifact.Name,
ContentType = artifact.ContentType,
Ciphertext = ByteString.CopyFrom(artifact.Ciphertext),
Nonce = ByteString.CopyFrom(artifact.Nonce),
Tag = ByteString.CopyFrom(artifact.Tag),
WrappedDek = ByteString.CopyFrom(artifact.WrappedDek),
WrapNonce = ByteString.CopyFrom(artifact.WrapNonce),
WrapTag = ByteString.CopyFrom(artifact.WrapTag),
KekId = artifact.KekId,
Revision = artifact.Revision,
CreatedUtc = Ts(artifact.CreatedUtc),
UpdatedUtc = Ts(artifact.UpdatedUtc),
Description = artifact.Description
};
}
/// <summary>Rehydrates a <see cref="SecretArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static SecretArtifact FromProto(SecretArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SecretArtifact(
dto.Name,
dto.ContentType,
dto.Ciphertext.ToByteArray(),
dto.Nonce.ToByteArray(),
dto.Tag.ToByteArray(),
dto.WrappedDek.ToByteArray(),
dto.WrapNonce.ToByteArray(),
dto.WrapTag.ToByteArray(),
dto.KekId,
dto.Revision,
Dto(dto.CreatedUtc),
Dto(dto.UpdatedUtc),
dto.Description);
}
/// <summary>Projects a <see cref="SharedScriptArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static SharedScriptArtifactDto ToProto(SharedScriptArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new SharedScriptArtifactDto
{
Name = artifact.Name,
Code = artifact.Code,
ParameterDefinitions = OrEmpty(artifact.ParameterDefinitions),
ReturnDefinition = OrEmpty(artifact.ReturnDefinition)
};
}
/// <summary>Rehydrates a <see cref="SharedScriptArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static SharedScriptArtifact FromProto(SharedScriptArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SharedScriptArtifact(
dto.Name, dto.Code, NullIfEmpty(dto.ParameterDefinitions), NullIfEmpty(dto.ReturnDefinition));
}
/// <summary>Projects an <see cref="ExternalSystemArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static ExternalSystemArtifactDto ToProto(ExternalSystemArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new ExternalSystemArtifactDto
{
Name = artifact.Name,
EndpointUrl = artifact.EndpointUrl,
AuthType = artifact.AuthType,
AuthConfiguration = OrEmpty(artifact.AuthConfiguration),
MethodDefinitionsJson = OrEmpty(artifact.MethodDefinitionsJson),
TimeoutSeconds = artifact.TimeoutSeconds,
MaxRetries = artifact.MaxRetries,
RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay)
};
}
/// <summary>Rehydrates an <see cref="ExternalSystemArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static ExternalSystemArtifact FromProto(ExternalSystemArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ExternalSystemArtifact(
dto.Name,
dto.EndpointUrl,
dto.AuthType,
NullIfEmpty(dto.AuthConfiguration),
NullIfEmpty(dto.MethodDefinitionsJson),
dto.TimeoutSeconds,
dto.MaxRetries,
dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero);
}
/// <summary>Projects a <see cref="DatabaseConnectionArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static DatabaseConnectionArtifactDto ToProto(DatabaseConnectionArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new DatabaseConnectionArtifactDto
{
Name = artifact.Name,
ConnectionString = artifact.ConnectionString,
MaxRetries = artifact.MaxRetries,
RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay)
};
}
/// <summary>Rehydrates a <see cref="DatabaseConnectionArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static DatabaseConnectionArtifact FromProto(DatabaseConnectionArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DatabaseConnectionArtifact(
dto.Name, dto.ConnectionString, dto.MaxRetries, dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero);
}
/// <summary>Projects a <see cref="NotificationListArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static NotificationListArtifactDto ToProto(NotificationListArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
var dto = new NotificationListArtifactDto { Name = artifact.Name };
dto.RecipientEmails.AddRange(artifact.RecipientEmails);
return dto;
}
/// <summary>Rehydrates a <see cref="NotificationListArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static NotificationListArtifact FromProto(NotificationListArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new NotificationListArtifact(dto.Name, dto.RecipientEmails.ToList());
}
/// <summary>Projects a <see cref="DataConnectionArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static DataConnectionArtifactDto ToProto(DataConnectionArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new DataConnectionArtifactDto
{
Name = artifact.Name,
Protocol = artifact.Protocol,
PrimaryConfigurationJson = OrEmpty(artifact.PrimaryConfigurationJson),
BackupConfigurationJson = OrEmpty(artifact.BackupConfigurationJson),
FailoverRetryCount = artifact.FailoverRetryCount
};
}
/// <summary>Rehydrates a <see cref="DataConnectionArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static DataConnectionArtifact FromProto(DataConnectionArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DataConnectionArtifact(
dto.Name,
dto.Protocol,
NullIfEmpty(dto.PrimaryConfigurationJson),
NullIfEmpty(dto.BackupConfigurationJson),
dto.FailoverRetryCount);
}
/// <summary>Projects an <see cref="SmtpConfigurationArtifact"/> onto the wire.</summary>
/// <param name="artifact">The artifact to project.</param>
/// <returns>The wire message.</returns>
public static SmtpConfigurationArtifactDto ToProto(SmtpConfigurationArtifact artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new SmtpConfigurationArtifactDto
{
Name = artifact.Name,
Server = artifact.Server,
Port = artifact.Port,
AuthMode = artifact.AuthMode,
FromAddress = artifact.FromAddress,
Username = OrEmpty(artifact.Username),
Password = OrEmpty(artifact.Password),
OauthConfig = OrEmpty(artifact.OAuthConfig)
};
}
/// <summary>Rehydrates an <see cref="SmtpConfigurationArtifact"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded artifact.</returns>
public static SmtpConfigurationArtifact FromProto(SmtpConfigurationArtifactDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SmtpConfigurationArtifact(
dto.Name,
dto.Server,
dto.Port,
dto.AuthMode,
dto.FromAddress,
NullIfEmpty(dto.Username),
NullIfEmpty(dto.Password),
NullIfEmpty(dto.OauthConfig));
}
// ── Lifecycle replies ──
/// <summary>Projects a <see cref="DeploymentStatusResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static DeploymentStatusResponseDto ToProto(DeploymentStatusResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new DeploymentStatusResponseDto
{
DeploymentId = response.DeploymentId,
InstanceUniqueName = response.InstanceUniqueName,
Status = ToProto(response.Status),
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="DeploymentStatusResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static DeploymentStatusResponse FromProto(DeploymentStatusResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DeploymentStatusResponse(
dto.DeploymentId,
dto.InstanceUniqueName,
FromProto(dto.Status),
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
/// <summary>Projects an <see cref="InstanceLifecycleResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static InstanceLifecycleResponseDto ToProto(InstanceLifecycleResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new InstanceLifecycleResponseDto
{
CommandId = response.CommandId,
InstanceUniqueName = response.InstanceUniqueName,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates an <see cref="InstanceLifecycleResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static InstanceLifecycleResponse FromProto(InstanceLifecycleResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new InstanceLifecycleResponse(
dto.CommandId,
dto.InstanceUniqueName,
dto.Success,
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DeploymentStateQueryResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static DeploymentStateQueryResponseDto ToProto(DeploymentStateQueryResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new DeploymentStateQueryResponseDto
{
CorrelationId = response.CorrelationId,
InstanceUniqueName = response.InstanceUniqueName,
IsDeployed = response.IsDeployed,
AppliedDeploymentId = OrEmpty(response.AppliedDeploymentId),
AppliedRevisionHash = OrEmpty(response.AppliedRevisionHash),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="DeploymentStateQueryResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static DeploymentStateQueryResponse FromProto(DeploymentStateQueryResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DeploymentStateQueryResponse(
dto.CorrelationId,
dto.InstanceUniqueName,
dto.IsDeployed,
NullIfEmpty(dto.AppliedDeploymentId),
NullIfEmpty(dto.AppliedRevisionHash),
Dto(dto.Timestamp));
}
/// <summary>Projects an <see cref="ArtifactDeploymentResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static ArtifactDeploymentResponseDto ToProto(ArtifactDeploymentResponse response)
{
ArgumentNullException.ThrowIfNull(response);
var dto = new ArtifactDeploymentResponseDto
{
DeploymentId = response.DeploymentId,
SiteId = response.SiteId,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
if (response.SecretsApplied.HasValue)
{
dto.SecretsApplied = response.SecretsApplied.Value;
}
if (response.SecretsRemoved is { Count: > 0 })
{
dto.SecretsRemoved.AddRange(response.SecretsRemoved);
}
return dto;
}
/// <summary>Rehydrates an <see cref="ArtifactDeploymentResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static ArtifactDeploymentResponse FromProto(ArtifactDeploymentResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ArtifactDeploymentResponse(
dto.DeploymentId, dto.SiteId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp),
dto.SecretsApplied,
dto.SecretsRemoved.Count > 0 ? dto.SecretsRemoved.ToList() : null);
}
// ─────────────────────────────────────────────────────────────────────
// RPC 2 — ExecuteOpcUa: envelopes
// ─────────────────────────────────────────────────────────────────────
/// <summary>Packs an OPC UA-group command into its request envelope.</summary>
/// <param name="command">One of the eight OPC UA-group commands.</param>
/// <returns>The populated request envelope.</returns>
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
public static OpcUaRequest ToOpcUaRequest(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
BrowseNodeCommand c => new OpcUaRequest { BrowseNode = ToProto(c) },
SearchAddressSpaceCommand c => new OpcUaRequest { SearchAddressSpace = ToProto(c) },
ReadTagValuesCommand c => new OpcUaRequest { ReadTagValues = ToProto(c) },
VerifyEndpointCommand c => new OpcUaRequest { VerifyEndpoint = ToProto(c) },
TrustServerCertCommand c => new OpcUaRequest { TrustServerCert = ToProto(c) },
ListServerCertsCommand c => new OpcUaRequest { ListServerCerts = ToProto(c) },
RemoveServerCertCommand c => new OpcUaRequest { RemoveServerCert = ToProto(c) },
WriteTagRequest c => new OpcUaRequest { WriteTag = ToProto(c) },
_ => throw NotInGroup(command, SiteCommandGroup.OpcUa)
};
}
/// <summary>Unpacks an OPC UA-group request envelope back to its command record.</summary>
/// <param name="request">The request envelope received on the wire.</param>
/// <returns>The decoded command record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromOpcUaRequest(OpcUaRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return request.CommandCase switch
{
OpcUaRequest.CommandOneofCase.BrowseNode => FromProto(request.BrowseNode),
OpcUaRequest.CommandOneofCase.SearchAddressSpace => FromProto(request.SearchAddressSpace),
OpcUaRequest.CommandOneofCase.ReadTagValues => FromProto(request.ReadTagValues),
OpcUaRequest.CommandOneofCase.VerifyEndpoint => FromProto(request.VerifyEndpoint),
OpcUaRequest.CommandOneofCase.TrustServerCert => FromProto(request.TrustServerCert),
OpcUaRequest.CommandOneofCase.ListServerCerts => FromProto(request.ListServerCerts),
OpcUaRequest.CommandOneofCase.RemoveServerCert => FromProto(request.RemoveServerCert),
OpcUaRequest.CommandOneofCase.WriteTag => FromProto(request.WriteTag),
_ => throw UnknownCase(request.CommandCase, nameof(OpcUaRequest))
};
}
/// <summary>Packs an OPC UA-group reply into its reply envelope.</summary>
/// <param name="reply">One of the six OPC UA-group reply records.</param>
/// <returns>The populated reply envelope.</returns>
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
public static OpcUaReply ToOpcUaReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
BrowseNodeResult r => new OpcUaReply { BrowseNode = ToProto(r) },
SearchAddressSpaceResult r => new OpcUaReply { SearchAddressSpace = ToProto(r) },
ReadTagValuesResult r => new OpcUaReply { ReadTagValues = ToProto(r) },
VerifyEndpointResult r => new OpcUaReply { VerifyEndpoint = ToProto(r) },
CertTrustResult r => new OpcUaReply { CertTrust = ToProto(r) },
WriteTagResponse r => new OpcUaReply { WriteTag = ToProto(r) },
_ => throw NotInGroup(reply, SiteCommandGroup.OpcUa)
};
}
/// <summary>Unpacks an OPC UA-group reply envelope back to its reply record.</summary>
/// <param name="reply">The reply envelope received on the wire.</param>
/// <returns>The decoded reply record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromOpcUaReply(OpcUaReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply.ReplyCase switch
{
OpcUaReply.ReplyOneofCase.BrowseNode => FromProto(reply.BrowseNode),
OpcUaReply.ReplyOneofCase.SearchAddressSpace => FromProto(reply.SearchAddressSpace),
OpcUaReply.ReplyOneofCase.ReadTagValues => FromProto(reply.ReadTagValues),
OpcUaReply.ReplyOneofCase.VerifyEndpoint => FromProto(reply.VerifyEndpoint),
OpcUaReply.ReplyOneofCase.CertTrust => FromProto(reply.CertTrust),
OpcUaReply.ReplyOneofCase.WriteTag => FromProto(reply.WriteTag),
_ => throw UnknownCase(reply.ReplyCase, nameof(OpcUaReply))
};
}
// ── OPC UA commands ──
/// <summary>Projects a <see cref="BrowseNodeCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static BrowseNodeCommandDto ToProto(BrowseNodeCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new BrowseNodeCommandDto
{
ConnectionName = command.ConnectionName,
ParentNodeId = OrEmpty(command.ParentNodeId),
ContinuationToken = OrEmpty(command.ContinuationToken),
SiteIdentifier = OrEmpty(command.SiteIdentifier)
};
}
/// <summary>Rehydrates a <see cref="BrowseNodeCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static BrowseNodeCommand FromProto(BrowseNodeCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new BrowseNodeCommand(
dto.ConnectionName,
NullIfEmpty(dto.ParentNodeId),
NullIfEmpty(dto.ContinuationToken),
NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="SearchAddressSpaceCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static SearchAddressSpaceCommandDto ToProto(SearchAddressSpaceCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new SearchAddressSpaceCommandDto
{
ConnectionName = command.ConnectionName,
Query = command.Query,
MaxDepth = command.MaxDepth,
MaxResults = command.MaxResults,
SiteIdentifier = OrEmpty(command.SiteIdentifier)
};
}
/// <summary>Rehydrates a <see cref="SearchAddressSpaceCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static SearchAddressSpaceCommand FromProto(SearchAddressSpaceCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SearchAddressSpaceCommand(
dto.ConnectionName, dto.Query, dto.MaxDepth, dto.MaxResults, NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="ReadTagValuesCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static ReadTagValuesCommandDto ToProto(ReadTagValuesCommand command)
{
ArgumentNullException.ThrowIfNull(command);
var dto = new ReadTagValuesCommandDto { ConnectionName = command.ConnectionName };
dto.TagPaths.AddRange(command.TagPaths);
return dto;
}
/// <summary>Rehydrates a <see cref="ReadTagValuesCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static ReadTagValuesCommand FromProto(ReadTagValuesCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ReadTagValuesCommand(dto.ConnectionName, dto.TagPaths.ToList());
}
/// <summary>Projects a <see cref="VerifyEndpointCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static VerifyEndpointCommandDto ToProto(VerifyEndpointCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new VerifyEndpointCommandDto
{
ConnectionName = command.ConnectionName,
Protocol = command.Protocol,
ConfigJson = command.ConfigJson,
SiteIdentifier = OrEmpty(command.SiteIdentifier)
};
}
/// <summary>Rehydrates a <see cref="VerifyEndpointCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static VerifyEndpointCommand FromProto(VerifyEndpointCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new VerifyEndpointCommand(
dto.ConnectionName, dto.Protocol, dto.ConfigJson, NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="TrustServerCertCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static TrustServerCertCommandDto ToProto(TrustServerCertCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new TrustServerCertCommandDto
{
ConnectionName = command.ConnectionName,
DerBase64 = command.DerBase64,
Thumbprint = command.Thumbprint,
SiteIdentifier = OrEmpty(command.SiteIdentifier)
};
}
/// <summary>Rehydrates a <see cref="TrustServerCertCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static TrustServerCertCommand FromProto(TrustServerCertCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new TrustServerCertCommand(
dto.ConnectionName, dto.DerBase64, dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="ListServerCertsCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static ListServerCertsCommandDto ToProto(ListServerCertsCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new ListServerCertsCommandDto { SiteIdentifier = OrEmpty(command.SiteIdentifier) };
}
/// <summary>Rehydrates a <see cref="ListServerCertsCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static ListServerCertsCommand FromProto(ListServerCertsCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ListServerCertsCommand(NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="RemoveServerCertCommand"/> onto the wire.</summary>
/// <param name="command">The command to project.</param>
/// <returns>The wire message.</returns>
public static RemoveServerCertCommandDto ToProto(RemoveServerCertCommand command)
{
ArgumentNullException.ThrowIfNull(command);
return new RemoveServerCertCommandDto
{
Thumbprint = command.Thumbprint,
SiteIdentifier = OrEmpty(command.SiteIdentifier)
};
}
/// <summary>Rehydrates a <see cref="RemoveServerCertCommand"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded command record.</returns>
public static RemoveServerCertCommand FromProto(RemoveServerCertCommandDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RemoveServerCertCommand(dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier));
}
/// <summary>Projects a <see cref="WriteTagRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static WriteTagRequestDto ToProto(WriteTagRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new WriteTagRequestDto
{
CorrelationId = request.CorrelationId,
ConnectionName = request.ConnectionName,
TagPath = request.TagPath,
Value = LooseValueCodec.ToProtoOrNull(request.Value),
Timestamp = Ts(request.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="WriteTagRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static WriteTagRequest FromProto(WriteTagRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new WriteTagRequest(
dto.CorrelationId,
dto.ConnectionName,
dto.TagPath,
LooseValueCodec.FromProto(dto.Value),
Dto(dto.Timestamp));
}
// ── OPC UA replies ──
/// <summary>Projects a <see cref="BrowseNodeResult"/> onto the wire.</summary>
/// <param name="result">The reply to project.</param>
/// <returns>The wire message.</returns>
public static BrowseNodeResultDto ToProto(BrowseNodeResult result)
{
ArgumentNullException.ThrowIfNull(result);
var dto = new BrowseNodeResultDto
{
Truncated = result.Truncated,
ContinuationToken = OrEmpty(result.ContinuationToken)
};
dto.Children.AddRange(result.Children.Select(ToProto));
if (result.Failure is not null)
{
dto.Failure = ToProto(result.Failure);
}
return dto;
}
/// <summary>Rehydrates a <see cref="BrowseNodeResult"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static BrowseNodeResult FromProto(BrowseNodeResultDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new BrowseNodeResult(
dto.Children.Select(FromProto).ToList(),
dto.Truncated,
dto.Failure is null ? null : FromProto(dto.Failure),
NullIfEmpty(dto.ContinuationToken));
}
/// <summary>Projects a <see cref="BrowseNode"/> onto the wire.</summary>
/// <param name="node">The address-space node to project.</param>
/// <returns>The wire message.</returns>
public static BrowseNodeDto ToProto(BrowseNode node)
{
ArgumentNullException.ThrowIfNull(node);
var dto = new BrowseNodeDto
{
NodeId = node.NodeId,
DisplayName = node.DisplayName,
NodeClass = ToProto(node.NodeClass),
HasChildren = node.HasChildren,
DataType = OrEmpty(node.DataType)
};
if (node.ValueRank.HasValue)
{
dto.ValueRank = node.ValueRank.Value;
}
if (node.Writable.HasValue)
{
dto.Writable = node.Writable.Value;
}
return dto;
}
/// <summary>Rehydrates a <see cref="BrowseNode"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded address-space node.</returns>
public static BrowseNode FromProto(BrowseNodeDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new BrowseNode(
dto.NodeId,
dto.DisplayName,
FromProto(dto.NodeClass),
dto.HasChildren,
NullIfEmpty(dto.DataType),
dto.ValueRank,
dto.Writable);
}
/// <summary>Projects a <see cref="BrowseFailure"/> onto the wire.</summary>
/// <param name="failure">The structured browse failure to project.</param>
/// <returns>The wire message.</returns>
public static BrowseFailureDto ToProto(BrowseFailure failure)
{
ArgumentNullException.ThrowIfNull(failure);
return new BrowseFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message };
}
/// <summary>Rehydrates a <see cref="BrowseFailure"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded structured browse failure.</returns>
public static BrowseFailure FromProto(BrowseFailureDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new BrowseFailure(FromProto(dto.Kind), dto.Message);
}
/// <summary>Projects a <see cref="SearchAddressSpaceResult"/> onto the wire.</summary>
/// <param name="result">The reply to project.</param>
/// <returns>The wire message.</returns>
public static SearchAddressSpaceResultDto ToProto(SearchAddressSpaceResult result)
{
ArgumentNullException.ThrowIfNull(result);
var dto = new SearchAddressSpaceResultDto { CapReached = result.CapReached };
dto.Matches.AddRange(result.Matches.Select(ToProto));
if (result.Failure is not null)
{
dto.Failure = ToProto(result.Failure);
}
return dto;
}
/// <summary>Rehydrates a <see cref="SearchAddressSpaceResult"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static SearchAddressSpaceResult FromProto(SearchAddressSpaceResultDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SearchAddressSpaceResult(
dto.Matches.Select(FromProto).ToList(),
dto.CapReached,
dto.Failure is null ? null : FromProto(dto.Failure));
}
/// <summary>Projects an <see cref="AddressSpaceMatch"/> onto the wire.</summary>
/// <param name="match">The search match to project.</param>
/// <returns>The wire message.</returns>
public static AddressSpaceMatchDto ToProto(AddressSpaceMatch match)
{
ArgumentNullException.ThrowIfNull(match);
return new AddressSpaceMatchDto { Node = ToProto(match.Node), Path = match.Path };
}
/// <summary>Rehydrates an <see cref="AddressSpaceMatch"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded search match.</returns>
public static AddressSpaceMatch FromProto(AddressSpaceMatchDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new AddressSpaceMatch(FromProto(dto.Node), dto.Path);
}
/// <summary>Projects a <see cref="ReadTagValuesResult"/> onto the wire.</summary>
/// <param name="result">The reply to project.</param>
/// <returns>The wire message.</returns>
public static ReadTagValuesResultDto ToProto(ReadTagValuesResult result)
{
ArgumentNullException.ThrowIfNull(result);
var dto = new ReadTagValuesResultDto();
dto.Outcomes.AddRange(result.Outcomes.Select(ToProto));
if (result.Failure is not null)
{
dto.Failure = ToProto(result.Failure);
}
return dto;
}
/// <summary>Rehydrates a <see cref="ReadTagValuesResult"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static ReadTagValuesResult FromProto(ReadTagValuesResultDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ReadTagValuesResult(
dto.Outcomes.Select(FromProto).ToList(),
dto.Failure is null ? null : FromProto(dto.Failure));
}
/// <summary>Projects a <see cref="TagReadOutcome"/> onto the wire.</summary>
/// <param name="outcome">The per-tag read outcome to project.</param>
/// <returns>The wire message.</returns>
public static TagReadOutcomeDto ToProto(TagReadOutcome outcome)
{
ArgumentNullException.ThrowIfNull(outcome);
return new TagReadOutcomeDto
{
TagPath = outcome.TagPath,
Success = outcome.Success,
Value = LooseValueCodec.ToProtoOrNull(outcome.Value),
Quality = outcome.Quality,
Timestamp = Ts(outcome.Timestamp),
ErrorMessage = OrEmpty(outcome.ErrorMessage)
};
}
/// <summary>Rehydrates a <see cref="TagReadOutcome"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded per-tag read outcome.</returns>
public static TagReadOutcome FromProto(TagReadOutcomeDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new TagReadOutcome(
dto.TagPath,
dto.Success,
LooseValueCodec.FromProto(dto.Value),
dto.Quality,
Dto(dto.Timestamp),
NullIfEmpty(dto.ErrorMessage));
}
/// <summary>Projects a <see cref="ReadTagValuesFailure"/> onto the wire.</summary>
/// <param name="failure">The connection-level read failure to project.</param>
/// <returns>The wire message.</returns>
public static ReadTagValuesFailureDto ToProto(ReadTagValuesFailure failure)
{
ArgumentNullException.ThrowIfNull(failure);
return new ReadTagValuesFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message };
}
/// <summary>Rehydrates a <see cref="ReadTagValuesFailure"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded connection-level read failure.</returns>
public static ReadTagValuesFailure FromProto(ReadTagValuesFailureDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ReadTagValuesFailure(FromProto(dto.Kind), dto.Message);
}
/// <summary>Projects a <see cref="VerifyEndpointResult"/> onto the wire.</summary>
/// <param name="result">The reply to project.</param>
/// <returns>The wire message.</returns>
public static VerifyEndpointResultDto ToProto(VerifyEndpointResult result)
{
ArgumentNullException.ThrowIfNull(result);
var dto = new VerifyEndpointResultDto
{
Success = result.Success,
Error = OrEmpty(result.Error)
};
if (result.FailureKind.HasValue)
{
dto.FailureKind = new VerifyFailureKindValue { Value = ToProto(result.FailureKind.Value) };
}
if (result.Cert is not null)
{
dto.Cert = ToProto(result.Cert);
}
return dto;
}
/// <summary>Rehydrates a <see cref="VerifyEndpointResult"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static VerifyEndpointResult FromProto(VerifyEndpointResultDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new VerifyEndpointResult(
dto.Success,
dto.FailureKind is null ? null : FromProto(dto.FailureKind.Value),
NullIfEmpty(dto.Error),
dto.Cert is null ? null : FromProto(dto.Cert));
}
/// <summary>Projects a <see cref="ServerCertInfo"/> onto the wire.</summary>
/// <param name="cert">The captured server certificate to project.</param>
/// <returns>The wire message.</returns>
public static ServerCertInfoDto ToProto(ServerCertInfo cert)
{
ArgumentNullException.ThrowIfNull(cert);
return new ServerCertInfoDto
{
Thumbprint = cert.Thumbprint,
Subject = cert.Subject,
Issuer = cert.Issuer,
NotBeforeUtc = TsUtc(cert.NotBeforeUtc),
NotAfterUtc = TsUtc(cert.NotAfterUtc),
DerBase64 = cert.DerBase64
};
}
/// <summary>Rehydrates a <see cref="ServerCertInfo"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded captured server certificate.</returns>
public static ServerCertInfo FromProto(ServerCertInfoDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ServerCertInfo(
dto.Thumbprint,
dto.Subject,
dto.Issuer,
DtUtc(dto.NotBeforeUtc),
DtUtc(dto.NotAfterUtc),
dto.DerBase64);
}
/// <summary>Projects a <see cref="CertTrustResult"/> onto the wire.</summary>
/// <remarks>A null <see cref="CertTrustResult.Certs"/> (trust/remove) leaves the list wrapper unset.</remarks>
/// <param name="result">The reply to project.</param>
/// <returns>The wire message.</returns>
public static CertTrustResultDto ToProto(CertTrustResult result)
{
ArgumentNullException.ThrowIfNull(result);
var dto = new CertTrustResultDto { Success = result.Success, Error = OrEmpty(result.Error) };
if (result.Certs is not null)
{
var list = new TrustedCertInfoListDto();
list.Items.AddRange(result.Certs.Select(ToProto));
dto.Certs = list;
}
return dto;
}
/// <summary>Rehydrates a <see cref="CertTrustResult"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static CertTrustResult FromProto(CertTrustResultDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new CertTrustResult(
dto.Success,
NullIfEmpty(dto.Error),
dto.Certs is null ? null : dto.Certs.Items.Select(FromProto).ToList());
}
/// <summary>Projects a <see cref="TrustedCertInfo"/> onto the wire.</summary>
/// <param name="cert">The PKI store entry to project.</param>
/// <returns>The wire message.</returns>
public static TrustedCertInfoDto ToProto(TrustedCertInfo cert)
{
ArgumentNullException.ThrowIfNull(cert);
return new TrustedCertInfoDto
{
Thumbprint = cert.Thumbprint,
Subject = cert.Subject,
Issuer = cert.Issuer,
NotBeforeUtc = TsUtc(cert.NotBeforeUtc),
NotAfterUtc = TsUtc(cert.NotAfterUtc),
Rejected = cert.Rejected
};
}
/// <summary>Rehydrates a <see cref="TrustedCertInfo"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded PKI store entry.</returns>
public static TrustedCertInfo FromProto(TrustedCertInfoDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new TrustedCertInfo(
dto.Thumbprint,
dto.Subject,
dto.Issuer,
DtUtc(dto.NotBeforeUtc),
DtUtc(dto.NotAfterUtc),
dto.Rejected);
}
/// <summary>Projects a <see cref="WriteTagResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static WriteTagResponseDto ToProto(WriteTagResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new WriteTagResponseDto
{
CorrelationId = response.CorrelationId,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="WriteTagResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static WriteTagResponse FromProto(WriteTagResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new WriteTagResponse(
dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp));
}
// ─────────────────────────────────────────────────────────────────────
// RPC 3 — ExecuteQuery: envelopes
// ─────────────────────────────────────────────────────────────────────
/// <summary>Packs a query-group command into its request envelope.</summary>
/// <param name="command">One of the four query-group commands.</param>
/// <returns>The populated request envelope.</returns>
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
public static QueryRequest ToQueryRequest(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
EventLogQueryRequest c => new QueryRequest { EventLogQuery = ToProto(c) },
DebugSnapshotRequest c => new QueryRequest { DebugSnapshot = ToProto(c) },
SubscribeDebugViewRequest c => new QueryRequest { SubscribeDebugView = ToProto(c) },
UnsubscribeDebugViewRequest c => new QueryRequest { UnsubscribeDebugView = ToProto(c) },
_ => throw NotInGroup(command, SiteCommandGroup.Query)
};
}
/// <summary>Unpacks a query-group request envelope back to its command record.</summary>
/// <param name="request">The request envelope received on the wire.</param>
/// <returns>The decoded command record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromQueryRequest(QueryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return request.CommandCase switch
{
QueryRequest.CommandOneofCase.EventLogQuery => FromProto(request.EventLogQuery),
QueryRequest.CommandOneofCase.DebugSnapshot => FromProto(request.DebugSnapshot),
QueryRequest.CommandOneofCase.SubscribeDebugView => FromProto(request.SubscribeDebugView),
QueryRequest.CommandOneofCase.UnsubscribeDebugView => FromProto(request.UnsubscribeDebugView),
_ => throw UnknownCase(request.CommandCase, nameof(QueryRequest))
};
}
/// <summary>
/// Packs a query-group reply into its reply envelope. Pass
/// <see cref="UnsubscribeDebugViewAck"/> for the unsubscribe path, which is a
/// Tell today and carries no payload.
/// </summary>
/// <param name="reply">One of the query-group reply records.</param>
/// <returns>The populated reply envelope.</returns>
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
public static QueryReply ToQueryReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
EventLogQueryResponse r => new QueryReply { EventLogQuery = ToProto(r) },
DebugViewSnapshot r => new QueryReply { DebugViewSnapshot = ToProto(r) },
UnsubscribeDebugViewAck => new QueryReply { UnsubscribeDebugView = new UnsubscribeDebugViewAckDto() },
_ => throw NotInGroup(reply, SiteCommandGroup.Query)
};
}
/// <summary>Unpacks a query-group reply envelope back to its reply record.</summary>
/// <param name="reply">The reply envelope received on the wire.</param>
/// <returns>The decoded reply record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromQueryReply(QueryReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply.ReplyCase switch
{
QueryReply.ReplyOneofCase.EventLogQuery => FromProto(reply.EventLogQuery),
QueryReply.ReplyOneofCase.DebugViewSnapshot => FromProto(reply.DebugViewSnapshot),
QueryReply.ReplyOneofCase.UnsubscribeDebugView => UnsubscribeDebugViewAck.Instance,
_ => throw UnknownCase(reply.ReplyCase, nameof(QueryReply))
};
}
// ── Query commands ──
/// <summary>Projects an <see cref="EventLogQueryRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static EventLogQueryRequestDto ToProto(EventLogQueryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var dto = new EventLogQueryRequestDto
{
CorrelationId = request.CorrelationId,
SiteId = request.SiteId,
EventType = OrEmpty(request.EventType),
Severity = OrEmpty(request.Severity),
InstanceId = OrEmpty(request.InstanceId),
KeywordFilter = OrEmpty(request.KeywordFilter),
ContinuationToken = OrEmpty(request.ContinuationToken),
PageSize = request.PageSize,
Timestamp = Ts(request.Timestamp)
};
if (request.From.HasValue)
{
dto.From = Ts(request.From.Value);
}
if (request.To.HasValue)
{
dto.To = Ts(request.To.Value);
}
return dto;
}
/// <summary>Rehydrates an <see cref="EventLogQueryRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static EventLogQueryRequest FromProto(EventLogQueryRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new EventLogQueryRequest(
dto.CorrelationId,
dto.SiteId,
DtoOrNull(dto.From),
DtoOrNull(dto.To),
NullIfEmpty(dto.EventType),
NullIfEmpty(dto.Severity),
NullIfEmpty(dto.InstanceId),
NullIfEmpty(dto.KeywordFilter),
NullIfEmpty(dto.ContinuationToken),
dto.PageSize,
Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="DebugSnapshotRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static DebugSnapshotRequestDto ToProto(DebugSnapshotRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new DebugSnapshotRequestDto
{
InstanceUniqueName = request.InstanceUniqueName,
CorrelationId = request.CorrelationId,
// Additive field 3 — proto3 false default means an older site that does
// not know the field simply returns the full snapshot (correct, just fatter).
AlarmsOnly = request.AlarmsOnly
};
}
/// <summary>Rehydrates a <see cref="DebugSnapshotRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static DebugSnapshotRequest FromProto(DebugSnapshotRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DebugSnapshotRequest(dto.InstanceUniqueName, dto.CorrelationId, dto.AlarmsOnly);
}
/// <summary>Projects a <see cref="SubscribeDebugViewRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static SubscribeDebugViewRequestDto ToProto(SubscribeDebugViewRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new SubscribeDebugViewRequestDto
{
InstanceUniqueName = request.InstanceUniqueName,
CorrelationId = request.CorrelationId
};
}
/// <summary>Rehydrates a <see cref="SubscribeDebugViewRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static SubscribeDebugViewRequest FromProto(SubscribeDebugViewRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId);
}
/// <summary>Projects an <see cref="UnsubscribeDebugViewRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static UnsubscribeDebugViewRequestDto ToProto(UnsubscribeDebugViewRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new UnsubscribeDebugViewRequestDto
{
InstanceUniqueName = request.InstanceUniqueName,
CorrelationId = request.CorrelationId
};
}
/// <summary>Rehydrates an <see cref="UnsubscribeDebugViewRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static UnsubscribeDebugViewRequest FromProto(UnsubscribeDebugViewRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new UnsubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId);
}
// ── Query replies ──
/// <summary>Projects an <see cref="EventLogQueryResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static EventLogQueryResponseDto ToProto(EventLogQueryResponse response)
{
ArgumentNullException.ThrowIfNull(response);
var dto = new EventLogQueryResponseDto
{
CorrelationId = response.CorrelationId,
SiteId = response.SiteId,
ContinuationToken = OrEmpty(response.ContinuationToken),
HasMore = response.HasMore,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
dto.Entries.AddRange(response.Entries.Select(ToProto));
return dto;
}
/// <summary>Rehydrates an <see cref="EventLogQueryResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static EventLogQueryResponse FromProto(EventLogQueryResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new EventLogQueryResponse(
dto.CorrelationId,
dto.SiteId,
dto.Entries.Select(FromProto).ToList(),
NullIfEmpty(dto.ContinuationToken),
dto.HasMore,
dto.Success,
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
/// <summary>Projects an <see cref="EventLogEntry"/> onto the wire.</summary>
/// <param name="entry">The event-log row to project.</param>
/// <returns>The wire message.</returns>
public static EventLogEntryDto ToProto(EventLogEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
return new EventLogEntryDto
{
Id = entry.Id,
Timestamp = Ts(entry.Timestamp),
EventType = entry.EventType,
Severity = entry.Severity,
InstanceId = OrEmpty(entry.InstanceId),
Source = entry.Source,
Message = entry.Message,
Details = OrEmpty(entry.Details)
};
}
/// <summary>Rehydrates an <see cref="EventLogEntry"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded event-log row.</returns>
public static EventLogEntry FromProto(EventLogEntryDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new EventLogEntry(
dto.Id,
Dto(dto.Timestamp),
dto.EventType,
dto.Severity,
NullIfEmpty(dto.InstanceId),
dto.Source,
dto.Message,
NullIfEmpty(dto.Details));
}
/// <summary>Projects a <see cref="DebugViewSnapshot"/> onto the wire.</summary>
/// <param name="snapshot">The reply to project.</param>
/// <returns>The wire message.</returns>
public static DebugViewSnapshotDto ToProto(DebugViewSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
var dto = new DebugViewSnapshotDto
{
InstanceUniqueName = snapshot.InstanceUniqueName,
SnapshotTimestamp = Ts(snapshot.SnapshotTimestamp),
InstanceNotFound = snapshot.InstanceNotFound
};
dto.AttributeValues.AddRange(snapshot.AttributeValues.Select(ToProto));
dto.AlarmStates.AddRange(snapshot.AlarmStates.Select(ToProto));
return dto;
}
/// <summary>Rehydrates a <see cref="DebugViewSnapshot"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static DebugViewSnapshot FromProto(DebugViewSnapshotDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DebugViewSnapshot(
dto.InstanceUniqueName,
dto.AttributeValues.Select(FromProto).ToList(),
dto.AlarmStates.Select(FromProto).ToList(),
Dto(dto.SnapshotTimestamp),
dto.InstanceNotFound);
}
/// <summary>Projects an <see cref="AttributeValueChanged"/> snapshot row onto the wire.</summary>
/// <param name="value">The attribute snapshot row to project.</param>
/// <returns>The wire message.</returns>
public static DebugAttributeValueDto ToProto(AttributeValueChanged value)
{
ArgumentNullException.ThrowIfNull(value);
return new DebugAttributeValueDto
{
InstanceUniqueName = value.InstanceUniqueName,
AttributePath = value.AttributePath,
AttributeName = value.AttributeName,
Value = LooseValueCodec.ToProtoOrNull(value.Value),
Quality = value.Quality,
Timestamp = Ts(value.Timestamp)
};
}
/// <summary>Rehydrates an <see cref="AttributeValueChanged"/> snapshot row from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded attribute snapshot row.</returns>
public static AttributeValueChanged FromProto(DebugAttributeValueDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new AttributeValueChanged(
dto.InstanceUniqueName,
dto.AttributePath,
dto.AttributeName,
LooseValueCodec.FromProto(dto.Value),
dto.Quality,
Dto(dto.Timestamp));
}
/// <summary>Projects an <see cref="AlarmStateChanged"/> snapshot row onto the wire.</summary>
/// <remarks>
/// <see cref="AlarmStateChanged.Condition"/> is only written when it differs
/// from the value the record itself derives from <c>State</c> + <c>Priority</c>.
/// The record stores the condition in a nullable backing field and synthesises
/// the computed default on read, so writing it unconditionally would turn an
/// implicit condition into an explicit one — indistinguishable through the
/// property, but a difference under record equality. Omitting the redundant
/// value keeps the round trip exact for computed alarms, which are the common
/// case, and is information-preserving for the rest.
/// </remarks>
/// <param name="alarm">The alarm snapshot row to project.</param>
/// <returns>The wire message.</returns>
public static DebugAlarmStateDto ToProto(AlarmStateChanged alarm)
{
ArgumentNullException.ThrowIfNull(alarm);
var dto = new DebugAlarmStateDto
{
InstanceUniqueName = alarm.InstanceUniqueName,
AlarmName = alarm.AlarmName,
State = ToProto(alarm.State),
Priority = alarm.Priority,
Timestamp = Ts(alarm.Timestamp),
Level = ToProto(alarm.Level),
Message = alarm.Message,
Kind = ToProto(alarm.Kind),
SourceReference = alarm.SourceReference,
AlarmTypeName = alarm.AlarmTypeName,
Category = alarm.Category,
OperatorUser = alarm.OperatorUser,
OperatorComment = alarm.OperatorComment,
CurrentValue = alarm.CurrentValue,
LimitValue = alarm.LimitValue,
NativeSourceCanonicalName = alarm.NativeSourceCanonicalName,
IsConfiguredPlaceholder = alarm.IsConfiguredPlaceholder,
IsMesReportable = alarm.IsMesReportable
};
if (alarm.Condition != AlarmConditionStateFactory.ForComputed(alarm.State, alarm.Priority))
{
dto.Condition = ToProto(alarm.Condition);
}
if (alarm.OriginalRaiseTime.HasValue)
{
dto.OriginalRaiseTime = Ts(alarm.OriginalRaiseTime.Value);
}
if (alarm.AckTime.HasValue)
{
dto.AckTime = Ts(alarm.AckTime.Value);
}
return dto;
}
/// <summary>Rehydrates an <see cref="AlarmStateChanged"/> snapshot row from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded alarm snapshot row.</returns>
public static AlarmStateChanged FromProto(DebugAlarmStateDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
var alarm = new AlarmStateChanged(
dto.InstanceUniqueName,
dto.AlarmName,
FromProto(dto.State),
dto.Priority,
Dto(dto.Timestamp))
{
Level = FromProto(dto.Level),
Message = dto.Message,
Kind = FromProto(dto.Kind),
SourceReference = dto.SourceReference,
AlarmTypeName = dto.AlarmTypeName,
Category = dto.Category,
OperatorUser = dto.OperatorUser,
OperatorComment = dto.OperatorComment,
OriginalRaiseTime = DtoOrNull(dto.OriginalRaiseTime),
CurrentValue = dto.CurrentValue,
LimitValue = dto.LimitValue,
NativeSourceCanonicalName = dto.NativeSourceCanonicalName,
IsConfiguredPlaceholder = dto.IsConfiguredPlaceholder,
IsMesReportable = dto.IsMesReportable,
AckTime = DtoOrNull(dto.AckTime)
};
// AlarmConditionStateDto carries Severity only; the raw priority (Gitea #129) travels as the
// row's own Priority, which the site always sets equal to Condition.Priority, so restore it here.
return dto.Condition is null
? alarm
: alarm with { Condition = FromProto(dto.Condition) with { Priority = dto.Priority } };
}
/// <summary>Projects an <see cref="AlarmConditionState"/> onto the wire.</summary>
/// <param name="condition">The unified alarm condition to project.</param>
/// <returns>The wire message.</returns>
public static AlarmConditionStateDto ToProto(AlarmConditionState condition)
{
ArgumentNullException.ThrowIfNull(condition);
var dto = new AlarmConditionStateDto
{
Active = condition.Active,
Acknowledged = condition.Acknowledged,
Shelve = ToProto(condition.Shelve),
Suppressed = condition.Suppressed,
Severity = condition.Severity
};
if (condition.Confirmed.HasValue)
{
dto.Confirmed = condition.Confirmed.Value;
}
return dto;
}
/// <summary>Rehydrates an <see cref="AlarmConditionState"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded unified alarm condition.</returns>
public static AlarmConditionState FromProto(AlarmConditionStateDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new AlarmConditionState(
dto.Active,
dto.Acknowledged,
dto.Confirmed,
FromProto(dto.Shelve),
dto.Suppressed,
dto.Severity);
}
// ─────────────────────────────────────────────────────────────────────
// RPC 4 — ExecuteParked: envelopes
// ─────────────────────────────────────────────────────────────────────
/// <summary>Packs a parked-group command into its request envelope.</summary>
/// <param name="command">One of the five parked-group commands.</param>
/// <returns>The populated request envelope.</returns>
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
public static ParkedRequest ToParkedRequest(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
ParkedMessageQueryRequest c => new ParkedRequest { ParkedMessageQuery = ToProto(c) },
ParkedMessageRetryRequest c => new ParkedRequest { ParkedMessageRetry = ToProto(c) },
ParkedMessageDiscardRequest c => new ParkedRequest { ParkedMessageDiscard = ToProto(c) },
RetryParkedOperation c => new ParkedRequest { RetryParkedOperation = ToProto(c) },
DiscardParkedOperation c => new ParkedRequest { DiscardParkedOperation = ToProto(c) },
_ => throw NotInGroup(command, SiteCommandGroup.Parked)
};
}
/// <summary>Unpacks a parked-group request envelope back to its command record.</summary>
/// <param name="request">The request envelope received on the wire.</param>
/// <returns>The decoded command record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromParkedRequest(ParkedRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return request.CommandCase switch
{
ParkedRequest.CommandOneofCase.ParkedMessageQuery => FromProto(request.ParkedMessageQuery),
ParkedRequest.CommandOneofCase.ParkedMessageRetry => FromProto(request.ParkedMessageRetry),
ParkedRequest.CommandOneofCase.ParkedMessageDiscard => FromProto(request.ParkedMessageDiscard),
ParkedRequest.CommandOneofCase.RetryParkedOperation => FromProto(request.RetryParkedOperation),
ParkedRequest.CommandOneofCase.DiscardParkedOperation => FromProto(request.DiscardParkedOperation),
_ => throw UnknownCase(request.CommandCase, nameof(ParkedRequest))
};
}
/// <summary>Packs a parked-group reply into its reply envelope.</summary>
/// <param name="reply">One of the four parked-group reply records.</param>
/// <returns>The populated reply envelope.</returns>
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
public static ParkedReply ToParkedReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
ParkedMessageQueryResponse r => new ParkedReply { ParkedMessageQuery = ToProto(r) },
ParkedMessageRetryResponse r => new ParkedReply { ParkedMessageRetry = ToProto(r) },
ParkedMessageDiscardResponse r => new ParkedReply { ParkedMessageDiscard = ToProto(r) },
ParkedOperationActionAck r => new ParkedReply { ParkedOperationAction = ToProto(r) },
_ => throw NotInGroup(reply, SiteCommandGroup.Parked)
};
}
/// <summary>Unpacks a parked-group reply envelope back to its reply record.</summary>
/// <param name="reply">The reply envelope received on the wire.</param>
/// <returns>The decoded reply record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromParkedReply(ParkedReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply.ReplyCase switch
{
ParkedReply.ReplyOneofCase.ParkedMessageQuery => FromProto(reply.ParkedMessageQuery),
ParkedReply.ReplyOneofCase.ParkedMessageRetry => FromProto(reply.ParkedMessageRetry),
ParkedReply.ReplyOneofCase.ParkedMessageDiscard => FromProto(reply.ParkedMessageDiscard),
ParkedReply.ReplyOneofCase.ParkedOperationAction => FromProto(reply.ParkedOperationAction),
_ => throw UnknownCase(reply.ReplyCase, nameof(ParkedReply))
};
}
// ── Parked commands ──
/// <summary>Projects a <see cref="ParkedMessageQueryRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageQueryRequestDto ToProto(ParkedMessageQueryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new ParkedMessageQueryRequestDto
{
CorrelationId = request.CorrelationId,
SiteId = request.SiteId,
PageNumber = request.PageNumber,
PageSize = request.PageSize,
Timestamp = Ts(request.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageQueryRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static ParkedMessageQueryRequest FromProto(ParkedMessageQueryRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageQueryRequest(
dto.CorrelationId, dto.SiteId, dto.PageNumber, dto.PageSize, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="ParkedMessageRetryRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageRetryRequestDto ToProto(ParkedMessageRetryRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new ParkedMessageRetryRequestDto
{
CorrelationId = request.CorrelationId,
SiteId = request.SiteId,
MessageId = request.MessageId,
Timestamp = Ts(request.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageRetryRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static ParkedMessageRetryRequest FromProto(ParkedMessageRetryRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageRetryRequest(
dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="ParkedMessageDiscardRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageDiscardRequestDto ToProto(ParkedMessageDiscardRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new ParkedMessageDiscardRequestDto
{
CorrelationId = request.CorrelationId,
SiteId = request.SiteId,
MessageId = request.MessageId,
Timestamp = Ts(request.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageDiscardRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static ParkedMessageDiscardRequest FromProto(ParkedMessageDiscardRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageDiscardRequest(
dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="RetryParkedOperation"/> relay onto the wire.</summary>
/// <param name="command">The relay command to project.</param>
/// <returns>The wire message.</returns>
public static RetryParkedOperationDto ToProto(RetryParkedOperation command)
{
ArgumentNullException.ThrowIfNull(command);
return new RetryParkedOperationDto
{
CorrelationId = command.CorrelationId,
TrackedOperationId = command.TrackedOperationId.ToString()
};
}
/// <summary>Rehydrates a <see cref="RetryParkedOperation"/> relay from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded relay command.</returns>
public static RetryParkedOperation FromProto(RetryParkedOperationDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RetryParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId));
}
/// <summary>Projects a <see cref="DiscardParkedOperation"/> relay onto the wire.</summary>
/// <param name="command">The relay command to project.</param>
/// <returns>The wire message.</returns>
public static DiscardParkedOperationDto ToProto(DiscardParkedOperation command)
{
ArgumentNullException.ThrowIfNull(command);
return new DiscardParkedOperationDto
{
CorrelationId = command.CorrelationId,
TrackedOperationId = command.TrackedOperationId.ToString()
};
}
/// <summary>Rehydrates a <see cref="DiscardParkedOperation"/> relay from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded relay command.</returns>
public static DiscardParkedOperation FromProto(DiscardParkedOperationDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new DiscardParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId));
}
// ── Parked replies ──
/// <summary>Projects a <see cref="ParkedMessageQueryResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageQueryResponseDto ToProto(ParkedMessageQueryResponse response)
{
ArgumentNullException.ThrowIfNull(response);
var dto = new ParkedMessageQueryResponseDto
{
CorrelationId = response.CorrelationId,
SiteId = response.SiteId,
TotalCount = response.TotalCount,
PageNumber = response.PageNumber,
PageSize = response.PageSize,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
dto.Messages.AddRange(response.Messages.Select(ToProto));
return dto;
}
/// <summary>Rehydrates a <see cref="ParkedMessageQueryResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static ParkedMessageQueryResponse FromProto(ParkedMessageQueryResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageQueryResponse(
dto.CorrelationId,
dto.SiteId,
dto.Messages.Select(FromProto).ToList(),
dto.TotalCount,
dto.PageNumber,
dto.PageSize,
dto.Success,
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="ParkedMessageEntry"/> onto the wire.</summary>
/// <param name="entry">The parked store-and-forward row to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageEntryDto ToProto(ParkedMessageEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
return new ParkedMessageEntryDto
{
MessageId = entry.MessageId,
TargetSystem = entry.TargetSystem,
MethodName = entry.MethodName,
ErrorMessage = entry.ErrorMessage,
AttemptCount = entry.AttemptCount,
OriginalTimestamp = Ts(entry.OriginalTimestamp),
LastAttemptTimestamp = Ts(entry.LastAttemptTimestamp),
MaxAttempts = entry.MaxAttempts,
Category = ToProto(entry.Category),
OriginInstance = OrEmpty(entry.OriginInstance)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageEntry"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded parked store-and-forward row.</returns>
public static ParkedMessageEntry FromProto(ParkedMessageEntryDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageEntry(
dto.MessageId,
dto.TargetSystem,
dto.MethodName,
dto.ErrorMessage,
dto.AttemptCount,
Dto(dto.OriginalTimestamp),
Dto(dto.LastAttemptTimestamp),
dto.MaxAttempts,
FromProto(dto.Category),
NullIfEmpty(dto.OriginInstance));
}
/// <summary>Projects a <see cref="ParkedMessageRetryResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageRetryResponseDto ToProto(ParkedMessageRetryResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new ParkedMessageRetryResponseDto
{
CorrelationId = response.CorrelationId,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageRetryResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static ParkedMessageRetryResponse FromProto(ParkedMessageRetryResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageRetryResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage));
}
/// <summary>Projects a <see cref="ParkedMessageDiscardResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static ParkedMessageDiscardResponseDto ToProto(ParkedMessageDiscardResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new ParkedMessageDiscardResponseDto
{
CorrelationId = response.CorrelationId,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage)
};
}
/// <summary>Rehydrates a <see cref="ParkedMessageDiscardResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static ParkedMessageDiscardResponse FromProto(ParkedMessageDiscardResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedMessageDiscardResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage));
}
/// <summary>Projects a <see cref="ParkedOperationActionAck"/> onto the wire.</summary>
/// <param name="ack">The relay ack to project.</param>
/// <returns>The wire message.</returns>
public static ParkedOperationActionAckDto ToProto(ParkedOperationActionAck ack)
{
ArgumentNullException.ThrowIfNull(ack);
return new ParkedOperationActionAckDto
{
CorrelationId = ack.CorrelationId,
Applied = ack.Applied,
ErrorMessage = OrEmpty(ack.ErrorMessage)
};
}
/// <summary>Rehydrates a <see cref="ParkedOperationActionAck"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded relay ack.</returns>
public static ParkedOperationActionAck FromProto(ParkedOperationActionAckDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new ParkedOperationActionAck(dto.CorrelationId, dto.Applied, NullIfEmpty(dto.ErrorMessage));
}
// ─────────────────────────────────────────────────────────────────────
// RPC 5 — ExecuteRoute: envelopes
// ─────────────────────────────────────────────────────────────────────
/// <summary>Packs a route-group command into its request envelope.</summary>
/// <param name="command">One of the four route-group commands.</param>
/// <returns>The populated request envelope.</returns>
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
public static RouteRequest ToRouteRequest(object command)
{
ArgumentNullException.ThrowIfNull(command);
return command switch
{
RouteToCallRequest c => new RouteRequest { RouteToCall = ToProto(c) },
RouteToGetAttributesRequest c => new RouteRequest { RouteToGetAttributes = ToProto(c) },
RouteToSetAttributesRequest c => new RouteRequest { RouteToSetAttributes = ToProto(c) },
RouteToWaitForAttributeRequest c => new RouteRequest { RouteToWaitForAttribute = ToProto(c) },
_ => throw NotInGroup(command, SiteCommandGroup.Route)
};
}
/// <summary>Unpacks a route-group request envelope back to its command record.</summary>
/// <param name="request">The request envelope received on the wire.</param>
/// <returns>The decoded command record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromRouteRequest(RouteRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return request.CommandCase switch
{
RouteRequest.CommandOneofCase.RouteToCall => FromProto(request.RouteToCall),
RouteRequest.CommandOneofCase.RouteToGetAttributes => FromProto(request.RouteToGetAttributes),
RouteRequest.CommandOneofCase.RouteToSetAttributes => FromProto(request.RouteToSetAttributes),
RouteRequest.CommandOneofCase.RouteToWaitForAttribute => FromProto(request.RouteToWaitForAttribute),
_ => throw UnknownCase(request.CommandCase, nameof(RouteRequest))
};
}
/// <summary>Packs a route-group reply into its reply envelope.</summary>
/// <param name="reply">One of the four route-group reply records.</param>
/// <returns>The populated reply envelope.</returns>
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
public static RouteReply ToRouteReply(object reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply switch
{
RouteToCallResponse r => new RouteReply { RouteToCall = ToProto(r) },
RouteToGetAttributesResponse r => new RouteReply { RouteToGetAttributes = ToProto(r) },
RouteToSetAttributesResponse r => new RouteReply { RouteToSetAttributes = ToProto(r) },
RouteToWaitForAttributeResponse r => new RouteReply { RouteToWaitForAttribute = ToProto(r) },
_ => throw NotInGroup(reply, SiteCommandGroup.Route)
};
}
/// <summary>Unpacks a route-group reply envelope back to its reply record.</summary>
/// <param name="reply">The reply envelope received on the wire.</param>
/// <returns>The decoded reply record.</returns>
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
public static object FromRouteReply(RouteReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
return reply.ReplyCase switch
{
RouteReply.ReplyOneofCase.RouteToCall => FromProto(reply.RouteToCall),
RouteReply.ReplyOneofCase.RouteToGetAttributes => FromProto(reply.RouteToGetAttributes),
RouteReply.ReplyOneofCase.RouteToSetAttributes => FromProto(reply.RouteToSetAttributes),
RouteReply.ReplyOneofCase.RouteToWaitForAttribute => FromProto(reply.RouteToWaitForAttribute),
_ => throw UnknownCase(reply.ReplyCase, nameof(RouteReply))
};
}
// ── Route commands ──
/// <summary>Projects a <see cref="RouteToCallRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static RouteToCallRequestDto ToProto(RouteToCallRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return new RouteToCallRequestDto
{
CorrelationId = request.CorrelationId,
InstanceUniqueName = request.InstanceUniqueName,
ScriptName = request.ScriptName,
Parameters = LooseValueCodec.ToProtoMapOrNull(request.Parameters),
Timestamp = Ts(request.Timestamp),
ParentExecutionId = GuidToWire(request.ParentExecutionId),
Trace = request.Trace
};
}
/// <summary>Rehydrates a <see cref="RouteToCallRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static RouteToCallRequest FromProto(RouteToCallRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToCallRequest(
dto.CorrelationId,
dto.InstanceUniqueName,
dto.ScriptName,
LooseValueCodec.FromProtoMapOrNull(dto.Parameters),
Dto(dto.Timestamp),
GuidFromWire(dto.ParentExecutionId),
dto.Trace);
}
/// <summary>Projects a <see cref="RouteToGetAttributesRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static RouteToGetAttributesRequestDto ToProto(RouteToGetAttributesRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var dto = new RouteToGetAttributesRequestDto
{
CorrelationId = request.CorrelationId,
InstanceUniqueName = request.InstanceUniqueName,
Timestamp = Ts(request.Timestamp),
ParentExecutionId = GuidToWire(request.ParentExecutionId)
};
dto.AttributeNames.AddRange(request.AttributeNames);
return dto;
}
/// <summary>Rehydrates a <see cref="RouteToGetAttributesRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static RouteToGetAttributesRequest FromProto(RouteToGetAttributesRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToGetAttributesRequest(
dto.CorrelationId,
dto.InstanceUniqueName,
dto.AttributeNames.ToList(),
Dto(dto.Timestamp),
GuidFromWire(dto.ParentExecutionId));
}
/// <summary>Projects a <see cref="RouteToSetAttributesRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static RouteToSetAttributesRequestDto ToProto(RouteToSetAttributesRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var dto = new RouteToSetAttributesRequestDto
{
CorrelationId = request.CorrelationId,
InstanceUniqueName = request.InstanceUniqueName,
Timestamp = Ts(request.Timestamp),
ParentExecutionId = GuidToWire(request.ParentExecutionId),
Trace = request.Trace
};
foreach (var (key, value) in request.AttributeValues)
{
dto.AttributeValues[key] = value;
}
return dto;
}
/// <summary>Rehydrates a <see cref="RouteToSetAttributesRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static RouteToSetAttributesRequest FromProto(RouteToSetAttributesRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToSetAttributesRequest(
dto.CorrelationId,
dto.InstanceUniqueName,
dto.AttributeValues.ToDictionary(kv => kv.Key, kv => kv.Value),
Dto(dto.Timestamp),
GuidFromWire(dto.ParentExecutionId),
dto.Trace);
}
/// <summary>Projects a <see cref="RouteToWaitForAttributeRequest"/> onto the wire.</summary>
/// <param name="request">The request to project.</param>
/// <returns>The wire message.</returns>
public static RouteToWaitForAttributeRequestDto ToProto(RouteToWaitForAttributeRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var dto = new RouteToWaitForAttributeRequestDto
{
CorrelationId = request.CorrelationId,
InstanceUniqueName = request.InstanceUniqueName,
AttributeName = request.AttributeName,
Timeout = Duration.FromTimeSpan(request.Timeout),
Timestamp = Ts(request.Timestamp),
ParentExecutionId = GuidToWire(request.ParentExecutionId),
RequireGoodQuality = request.RequireGoodQuality
};
if (request.TargetValueEncoded is not null)
{
dto.TargetValueEncoded = request.TargetValueEncoded;
}
return dto;
}
/// <summary>Rehydrates a <see cref="RouteToWaitForAttributeRequest"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded request record.</returns>
public static RouteToWaitForAttributeRequest FromProto(RouteToWaitForAttributeRequestDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToWaitForAttributeRequest(
dto.CorrelationId,
dto.InstanceUniqueName,
dto.AttributeName,
dto.TargetValueEncoded,
dto.Timeout?.ToTimeSpan() ?? TimeSpan.Zero,
Dto(dto.Timestamp),
GuidFromWire(dto.ParentExecutionId),
dto.RequireGoodQuality);
}
// ── Route replies ──
/// <summary>Projects a <see cref="RouteToCallResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static RouteToCallResponseDto ToProto(RouteToCallResponse response)
{
ArgumentNullException.ThrowIfNull(response);
var dto = new RouteToCallResponseDto
{
CorrelationId = response.CorrelationId,
Success = response.Success,
ReturnValue = LooseValueCodec.ToProtoOrNull(response.ReturnValue),
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp),
// Flow Debugger (design 2026-09-05 §5.4): additive fields 6/7.
ChildExecutionId = GuidToWire(response.ChildExecutionId),
// Gitea #54: additive fields 8/9 — the site collector's truncation flags.
Truncated = response.Truncated,
LogsTruncated = response.LogsTruncated
};
if (response.TraceEvents is { Count: > 0 } traceEvents)
{
foreach (var evt in traceEvents)
{
dto.TraceEvents.Add(AuditEventDtoMapper.ToDto(evt));
}
}
return dto;
}
/// <summary>Rehydrates a <see cref="RouteToCallResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static RouteToCallResponse FromProto(RouteToCallResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToCallResponse(
dto.CorrelationId,
dto.Success,
LooseValueCodec.FromProto(dto.ReturnValue),
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp),
GuidFromWire(dto.ChildExecutionId),
// An untraced run — or an older site that never sets the field — sends no
// events, which maps to null so the UI can label the lane "site not traced".
dto.TraceEvents.Count == 0 ? null : dto.TraceEvents.Select(AuditEventDtoMapper.FromDto).ToList(),
dto.Truncated,
dto.LogsTruncated);
}
/// <summary>Projects a <see cref="RouteToGetAttributesResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static RouteToGetAttributesResponseDto ToProto(RouteToGetAttributesResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new RouteToGetAttributesResponseDto
{
CorrelationId = response.CorrelationId,
Values = LooseValueCodec.ToProtoMap(response.Values),
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="RouteToGetAttributesResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static RouteToGetAttributesResponse FromProto(RouteToGetAttributesResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToGetAttributesResponse(
dto.CorrelationId,
LooseValueCodec.FromProtoMap(dto.Values),
dto.Success,
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="RouteToSetAttributesResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static RouteToSetAttributesResponseDto ToProto(RouteToSetAttributesResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new RouteToSetAttributesResponseDto
{
CorrelationId = response.CorrelationId,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="RouteToSetAttributesResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static RouteToSetAttributesResponse FromProto(RouteToSetAttributesResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToSetAttributesResponse(
dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp));
}
/// <summary>Projects a <see cref="RouteToWaitForAttributeResponse"/> onto the wire.</summary>
/// <param name="response">The reply to project.</param>
/// <returns>The wire message.</returns>
public static RouteToWaitForAttributeResponseDto ToProto(RouteToWaitForAttributeResponse response)
{
ArgumentNullException.ThrowIfNull(response);
return new RouteToWaitForAttributeResponseDto
{
CorrelationId = response.CorrelationId,
Matched = response.Matched,
Value = LooseValueCodec.ToProtoOrNull(response.Value),
Quality = OrEmpty(response.Quality),
TimedOut = response.TimedOut,
Success = response.Success,
ErrorMessage = OrEmpty(response.ErrorMessage),
Timestamp = Ts(response.Timestamp)
};
}
/// <summary>Rehydrates a <see cref="RouteToWaitForAttributeResponse"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded reply record.</returns>
public static RouteToWaitForAttributeResponse FromProto(RouteToWaitForAttributeResponseDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new RouteToWaitForAttributeResponse(
dto.CorrelationId,
dto.Matched,
LooseValueCodec.FromProto(dto.Value),
NullIfEmpty(dto.Quality),
dto.TimedOut,
dto.Success,
NullIfEmpty(dto.ErrorMessage),
Dto(dto.Timestamp));
}
// ─────────────────────────────────────────────────────────────────────
// RPC 6 — TriggerFailover
// ─────────────────────────────────────────────────────────────────────
/// <summary>Projects a <see cref="TriggerSiteFailover"/> onto the wire.</summary>
/// <param name="command">The relay command to project.</param>
/// <returns>The wire message.</returns>
public static TriggerSiteFailoverDto ToProto(TriggerSiteFailover command)
{
ArgumentNullException.ThrowIfNull(command);
return new TriggerSiteFailoverDto
{
CorrelationId = command.CorrelationId,
SiteId = command.SiteId
};
}
/// <summary>Rehydrates a <see cref="TriggerSiteFailover"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded relay command.</returns>
public static TriggerSiteFailover FromProto(TriggerSiteFailoverDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new TriggerSiteFailover(dto.CorrelationId, dto.SiteId);
}
/// <summary>Projects a <see cref="SiteFailoverAck"/> onto the wire.</summary>
/// <param name="ack">The relay ack to project.</param>
/// <returns>The wire message.</returns>
public static SiteFailoverAckDto ToProto(SiteFailoverAck ack)
{
ArgumentNullException.ThrowIfNull(ack);
return new SiteFailoverAckDto
{
CorrelationId = ack.CorrelationId,
Accepted = ack.Accepted,
TargetAddress = OrEmpty(ack.TargetAddress),
ErrorMessage = OrEmpty(ack.ErrorMessage)
};
}
/// <summary>Rehydrates a <see cref="SiteFailoverAck"/> from the wire.</summary>
/// <param name="dto">The wire message to decode.</param>
/// <returns>The decoded relay ack.</returns>
public static SiteFailoverAck FromProto(SiteFailoverAckDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SiteFailoverAck(
dto.CorrelationId,
dto.Accepted,
NullIfEmpty(dto.TargetAddress),
NullIfEmpty(dto.ErrorMessage));
}
// ─────────────────────────────────────────────────────────────────────
// Enum translation — explicit both ways, never by ordinal.
// ─────────────────────────────────────────────────────────────────────
/// <summary>Projects a <see cref="DeploymentStatus"/> onto its wire enum.</summary>
/// <param name="status">The status to project.</param>
/// <returns>The wire enum value.</returns>
public static DeploymentStatusDto ToProto(DeploymentStatus status) => status switch
{
DeploymentStatus.Pending => DeploymentStatusDto.Pending,
DeploymentStatus.InProgress => DeploymentStatusDto.InProgress,
DeploymentStatus.Success => DeploymentStatusDto.Success,
DeploymentStatus.Failed => DeploymentStatusDto.Failed,
_ => DeploymentStatusDto.Unspecified
};
/// <summary>Rehydrates a <see cref="DeploymentStatus"/>; unknown decodes to <c>Failed</c> (never claim success).</summary>
/// <param name="status">The wire enum value.</param>
/// <returns>The decoded status.</returns>
public static DeploymentStatus FromProto(DeploymentStatusDto status) => status switch
{
DeploymentStatusDto.Pending => DeploymentStatus.Pending,
DeploymentStatusDto.InProgress => DeploymentStatus.InProgress,
DeploymentStatusDto.Success => DeploymentStatus.Success,
_ => DeploymentStatus.Failed
};
/// <summary>Projects a <see cref="BrowseNodeClass"/> onto its wire enum.</summary>
/// <param name="nodeClass">The node class to project.</param>
/// <returns>The wire enum value.</returns>
public static BrowseNodeClassDto ToProto(BrowseNodeClass nodeClass) => nodeClass switch
{
BrowseNodeClass.Object => BrowseNodeClassDto.Object,
BrowseNodeClass.Variable => BrowseNodeClassDto.Variable,
BrowseNodeClass.Method => BrowseNodeClassDto.Method,
BrowseNodeClass.Other => BrowseNodeClassDto.Other,
_ => BrowseNodeClassDto.Unspecified
};
/// <summary>Rehydrates a <see cref="BrowseNodeClass"/>; unknown decodes to <c>Other</c>.</summary>
/// <param name="nodeClass">The wire enum value.</param>
/// <returns>The decoded node class.</returns>
public static BrowseNodeClass FromProto(BrowseNodeClassDto nodeClass) => nodeClass switch
{
BrowseNodeClassDto.Object => BrowseNodeClass.Object,
BrowseNodeClassDto.Variable => BrowseNodeClass.Variable,
BrowseNodeClassDto.Method => BrowseNodeClass.Method,
_ => BrowseNodeClass.Other
};
/// <summary>Projects a <see cref="BrowseFailureKind"/> onto its wire enum.</summary>
/// <param name="kind">The failure kind to project.</param>
/// <returns>The wire enum value.</returns>
public static BrowseFailureKindDto ToProto(BrowseFailureKind kind) => kind switch
{
BrowseFailureKind.ConnectionNotFound => BrowseFailureKindDto.ConnectionNotFound,
BrowseFailureKind.ConnectionNotConnected => BrowseFailureKindDto.ConnectionNotConnected,
BrowseFailureKind.NotBrowsable => BrowseFailureKindDto.NotBrowsable,
BrowseFailureKind.Timeout => BrowseFailureKindDto.Timeout,
BrowseFailureKind.ServerError => BrowseFailureKindDto.ServerError,
_ => BrowseFailureKindDto.Unspecified
};
/// <summary>Rehydrates a <see cref="BrowseFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
/// <param name="kind">The wire enum value.</param>
/// <returns>The decoded failure kind.</returns>
public static BrowseFailureKind FromProto(BrowseFailureKindDto kind) => kind switch
{
BrowseFailureKindDto.ConnectionNotFound => BrowseFailureKind.ConnectionNotFound,
BrowseFailureKindDto.ConnectionNotConnected => BrowseFailureKind.ConnectionNotConnected,
BrowseFailureKindDto.NotBrowsable => BrowseFailureKind.NotBrowsable,
BrowseFailureKindDto.Timeout => BrowseFailureKind.Timeout,
_ => BrowseFailureKind.ServerError
};
/// <summary>Projects a <see cref="ReadTagValuesFailureKind"/> onto its wire enum.</summary>
/// <param name="kind">The failure kind to project.</param>
/// <returns>The wire enum value.</returns>
public static ReadTagValuesFailureKindDto ToProto(ReadTagValuesFailureKind kind) => kind switch
{
ReadTagValuesFailureKind.ConnectionNotFound => ReadTagValuesFailureKindDto.ConnectionNotFound,
ReadTagValuesFailureKind.ConnectionNotConnected => ReadTagValuesFailureKindDto.ConnectionNotConnected,
ReadTagValuesFailureKind.Timeout => ReadTagValuesFailureKindDto.Timeout,
ReadTagValuesFailureKind.ServerError => ReadTagValuesFailureKindDto.ServerError,
_ => ReadTagValuesFailureKindDto.Unspecified
};
/// <summary>Rehydrates a <see cref="ReadTagValuesFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
/// <param name="kind">The wire enum value.</param>
/// <returns>The decoded failure kind.</returns>
public static ReadTagValuesFailureKind FromProto(ReadTagValuesFailureKindDto kind) => kind switch
{
ReadTagValuesFailureKindDto.ConnectionNotFound => ReadTagValuesFailureKind.ConnectionNotFound,
ReadTagValuesFailureKindDto.ConnectionNotConnected => ReadTagValuesFailureKind.ConnectionNotConnected,
ReadTagValuesFailureKindDto.Timeout => ReadTagValuesFailureKind.Timeout,
_ => ReadTagValuesFailureKind.ServerError
};
/// <summary>Projects a <see cref="VerifyFailureKind"/> onto its wire enum.</summary>
/// <param name="kind">The failure kind to project.</param>
/// <returns>The wire enum value.</returns>
public static VerifyFailureKindDto ToProto(VerifyFailureKind kind) => kind switch
{
VerifyFailureKind.Unreachable => VerifyFailureKindDto.Unreachable,
VerifyFailureKind.AuthFailed => VerifyFailureKindDto.AuthFailed,
VerifyFailureKind.UntrustedCertificate => VerifyFailureKindDto.UntrustedCertificate,
VerifyFailureKind.Timeout => VerifyFailureKindDto.Timeout,
VerifyFailureKind.ServerError => VerifyFailureKindDto.ServerError,
_ => VerifyFailureKindDto.Unspecified
};
/// <summary>Rehydrates a <see cref="VerifyFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
/// <param name="kind">The wire enum value.</param>
/// <returns>The decoded failure kind.</returns>
public static VerifyFailureKind FromProto(VerifyFailureKindDto kind) => kind switch
{
VerifyFailureKindDto.Unreachable => VerifyFailureKind.Unreachable,
VerifyFailureKindDto.AuthFailed => VerifyFailureKind.AuthFailed,
VerifyFailureKindDto.UntrustedCertificate => VerifyFailureKind.UntrustedCertificate,
VerifyFailureKindDto.Timeout => VerifyFailureKind.Timeout,
_ => VerifyFailureKind.ServerError
};
/// <summary>Projects a <see cref="StoreAndForwardCategory"/> onto its wire enum.</summary>
/// <param name="category">The category to project.</param>
/// <returns>The wire enum value.</returns>
public static StoreAndForwardCategoryDto ToProto(StoreAndForwardCategory category) => category switch
{
StoreAndForwardCategory.ExternalSystem => StoreAndForwardCategoryDto.ExternalSystem,
StoreAndForwardCategory.Notification => StoreAndForwardCategoryDto.Notification,
StoreAndForwardCategory.CachedDbWrite => StoreAndForwardCategoryDto.CachedDbWrite,
_ => StoreAndForwardCategoryDto.Unspecified
};
/// <summary>Rehydrates a <see cref="StoreAndForwardCategory"/>; unknown decodes to <c>ExternalSystem</c> (the DTO's own default).</summary>
/// <param name="category">The wire enum value.</param>
/// <returns>The decoded category.</returns>
public static StoreAndForwardCategory FromProto(StoreAndForwardCategoryDto category) => category switch
{
StoreAndForwardCategoryDto.Notification => StoreAndForwardCategory.Notification,
StoreAndForwardCategoryDto.CachedDbWrite => StoreAndForwardCategory.CachedDbWrite,
_ => StoreAndForwardCategory.ExternalSystem
};
/// <summary>Projects an <see cref="AlarmState"/> onto its wire enum.</summary>
/// <param name="state">The alarm state to project.</param>
/// <returns>The wire enum value.</returns>
public static AlarmStateDto ToProto(AlarmState state) => state switch
{
AlarmState.Active => AlarmStateDto.Active,
AlarmState.Normal => AlarmStateDto.Normal,
_ => AlarmStateDto.Unspecified
};
/// <summary>Rehydrates an <see cref="AlarmState"/>; unknown decodes to <c>Normal</c> (never invent an alarm).</summary>
/// <param name="state">The wire enum value.</param>
/// <returns>The decoded alarm state.</returns>
public static AlarmState FromProto(AlarmStateDto state) =>
state == AlarmStateDto.Active ? AlarmState.Active : AlarmState.Normal;
/// <summary>Projects an <see cref="AlarmLevel"/> onto its wire enum.</summary>
/// <param name="level">The alarm level to project.</param>
/// <returns>The wire enum value.</returns>
public static AlarmLevelDto ToProto(AlarmLevel level) => level switch
{
AlarmLevel.None => AlarmLevelDto.None,
AlarmLevel.Low => AlarmLevelDto.Low,
AlarmLevel.LowLow => AlarmLevelDto.LowLow,
AlarmLevel.High => AlarmLevelDto.High,
AlarmLevel.HighHigh => AlarmLevelDto.HighHigh,
_ => AlarmLevelDto.Unspecified
};
/// <summary>Rehydrates an <see cref="AlarmLevel"/>; unknown decodes to <c>None</c>.</summary>
/// <param name="level">The wire enum value.</param>
/// <returns>The decoded alarm level.</returns>
public static AlarmLevel FromProto(AlarmLevelDto level) => level switch
{
AlarmLevelDto.Low => AlarmLevel.Low,
AlarmLevelDto.LowLow => AlarmLevel.LowLow,
AlarmLevelDto.High => AlarmLevel.High,
AlarmLevelDto.HighHigh => AlarmLevel.HighHigh,
_ => AlarmLevel.None
};
/// <summary>Projects an <see cref="AlarmKind"/> onto its wire enum.</summary>
/// <param name="kind">The alarm kind to project.</param>
/// <returns>The wire enum value.</returns>
public static AlarmKindDto ToProto(AlarmKind kind) => kind switch
{
AlarmKind.Computed => AlarmKindDto.Computed,
AlarmKind.NativeOpcUa => AlarmKindDto.NativeOpcUa,
AlarmKind.NativeMxAccess => AlarmKindDto.NativeMxAccess,
_ => AlarmKindDto.Unspecified
};
/// <summary>Rehydrates an <see cref="AlarmKind"/>; unknown decodes to <c>Computed</c> (the record's own default).</summary>
/// <param name="kind">The wire enum value.</param>
/// <returns>The decoded alarm kind.</returns>
public static AlarmKind FromProto(AlarmKindDto kind) => kind switch
{
AlarmKindDto.NativeOpcUa => AlarmKind.NativeOpcUa,
AlarmKindDto.NativeMxAccess => AlarmKind.NativeMxAccess,
_ => AlarmKind.Computed
};
/// <summary>Projects an <see cref="AlarmShelveState"/> onto its wire enum.</summary>
/// <param name="shelve">The shelve state to project.</param>
/// <returns>The wire enum value.</returns>
public static AlarmShelveStateDto ToProto(AlarmShelveState shelve) => shelve switch
{
AlarmShelveState.Unshelved => AlarmShelveStateDto.Unshelved,
AlarmShelveState.OneShotShelved => AlarmShelveStateDto.OneShotShelved,
AlarmShelveState.TimedShelved => AlarmShelveStateDto.TimedShelved,
AlarmShelveState.PermanentShelved => AlarmShelveStateDto.PermanentShelved,
_ => AlarmShelveStateDto.Unspecified
};
/// <summary>
/// Rehydrates an <see cref="AlarmShelveState"/>; unknown decodes to
/// <c>Unshelved</c> — the same safe default <see cref="AlarmShelveStateCodec"/>
/// applies on the streaming path.
/// </summary>
/// <param name="shelve">The wire enum value.</param>
/// <returns>The decoded shelve state.</returns>
public static AlarmShelveState FromProto(AlarmShelveStateDto shelve) => shelve switch
{
AlarmShelveStateDto.OneShotShelved => AlarmShelveState.OneShotShelved,
AlarmShelveStateDto.TimedShelved => AlarmShelveState.TimedShelved,
AlarmShelveStateDto.PermanentShelved => AlarmShelveState.PermanentShelved,
_ => AlarmShelveState.Unshelved
};
// ─────────────────────────────────────────────────────────────────────
// Primitive helpers
// ─────────────────────────────────────────────────────────────────────
private static Timestamp Ts(DateTimeOffset value) => Timestamp.FromDateTimeOffset(value);
private static DateTimeOffset Dto(Timestamp? value) =>
value?.ToDateTimeOffset() ?? default;
private static DateTimeOffset? DtoOrNull(Timestamp? value) =>
value?.ToDateTimeOffset();
private static Timestamp TsUtc(DateTime value) => Timestamp.FromDateTime(EnsureUtc(value));
private static DateTime DtUtc(Timestamp? value) => value?.ToDateTime() ?? default;
private static DateTime EnsureUtc(DateTime value) =>
value.Kind == DateTimeKind.Utc
? value
: DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc);
private static string OrEmpty(string? value) => value ?? string.Empty;
private static string? NullIfEmpty(string value) => string.IsNullOrEmpty(value) ? null : value;
private static string GuidToWire(Guid? value) => value?.ToString("D") ?? string.Empty;
private static Guid? GuidFromWire(string value) =>
Guid.TryParse(value, out var parsed) ? parsed : null;
private static TrackedOperationId TrackedId(string value) =>
TrackedOperationId.TryParse(value, out var parsed) ? parsed : default;
private static ArgumentException NotInGroup(object message, SiteCommandGroup group) =>
new($"'{message.GetType().Name}' is not part of the {group} RPC group.", nameof(message));
private static NotSupportedException UnknownCase<TCase>(TCase oneofCase, string envelope)
where TCase : struct, Enum =>
new($"{envelope} carries oneof case '{oneofCase}', which this build does not understand. "
+ "The peer is running a newer contract (message evolution is additive-only).");
}
/// <summary>
/// Marker reply for the fire-and-forget <c>UnsubscribeDebugView</c> command.
/// </summary>
/// <remarks>
/// The Akka path Tells the unsubscribe and expects nothing back, but a unary RPC
/// must answer something. This singleton is what the dispatcher returns and what
/// <see cref="SiteCommandDtoMapper.FromQueryReply"/> produces, so the two
/// transports present the same shape; the central transport discards it and keeps
/// the caller-visible fire-and-forget semantics.
/// </remarks>
public sealed record UnsubscribeDebugViewAck
{
private UnsubscribeDebugViewAck()
{
}
/// <summary>The single instance.</summary>
public static UnsubscribeDebugViewAck Instance { get; } = new();
}