9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
130 lines
6.3 KiB
C#
130 lines
6.3 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
|
|
/// <summary>
|
|
/// Canonical bridge for Site Call Audit operational rows between the
|
|
/// wire-format <see cref="SiteCallOperationalDto"/> exchanged on the
|
|
/// <c>CachedCallTelemetry</c> packet and the in-process <see cref="SiteCall"/>
|
|
/// persistence entity central writes into the <c>SiteCalls</c> table.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This mapper lives in <c>ZB.MOM.WW.ScadaBridge.Communication</c> (which owns the generated
|
|
/// <see cref="SiteCallOperationalDto"/> and references <c>Commons</c> for
|
|
/// <see cref="SiteCall"/>) so both <c>SiteStreamGrpcServer</c> and
|
|
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> can share one implementation without the
|
|
/// project-reference cycle that would result from hosting it in
|
|
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> (AuditLog → Communication, never the reverse).
|
|
/// Mirrors the sibling <see cref="AuditEventDtoMapper"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// Two directions are provided. <see cref="FromDto"/> rehydrates the central
|
|
/// <see cref="SiteCall"/> entity central writes into the <c>SiteCalls</c> table.
|
|
/// <see cref="ToDto"/> projects a site-local <see cref="SiteCallOperational"/>
|
|
/// onto the wire — used by the Site Call Audit <c>PullSiteCalls</c>
|
|
/// reconciliation handler (the central→site self-heal pull). The
|
|
/// <see cref="SiteCall"/> entity itself is never mapped back onto the wire:
|
|
/// sites emit operational state from <see cref="SiteCallOperational"/>, never
|
|
/// from the central <see cref="SiteCall"/>, so a <c>SiteCall</c>→DTO method
|
|
/// would be dead code.
|
|
/// </para>
|
|
/// <para>
|
|
/// String nullability convention: proto3 scalar strings cannot be absent, so the
|
|
/// optional <see cref="SiteCall.LastError"/> rehydrates from an empty string back
|
|
/// to null. The optional <c>HttpStatus</c> and <c>TerminalAtUtc</c> use proto
|
|
/// wrappers so they preserve true null semantics.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class SiteCallDtoMapper
|
|
{
|
|
/// <summary>
|
|
/// Reconstructs a <see cref="SiteCall"/> persistence entity from its
|
|
/// wire-format DTO. An empty <c>LastError</c> rehydrates as null; absent
|
|
/// <c>HttpStatus</c>/<c>TerminalAtUtc</c> wrappers stay null.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="SiteCall.IngestedAtUtc"/> is stamped here as a placeholder
|
|
/// (<see cref="DateTime.UtcNow"/>); the central ingest actor overwrites it
|
|
/// inside the dual-write transaction so the AuditLog and SiteCalls rows
|
|
/// share one instant. The value sent on the wire is informational only.
|
|
/// </remarks>
|
|
/// <param name="dto">The wire-format site call DTO to map.</param>
|
|
/// <returns>A <see cref="SiteCall"/> entity populated from the DTO fields.</returns>
|
|
public static SiteCall FromDto(SiteCallOperationalDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SiteCall
|
|
{
|
|
TrackedOperationId = TrackedOperationId.Parse(dto.TrackedOperationId),
|
|
Channel = dto.Channel,
|
|
Target = dto.Target,
|
|
SourceSite = dto.SourceSite,
|
|
SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode,
|
|
Status = dto.Status,
|
|
RetryCount = dto.RetryCount,
|
|
LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError,
|
|
HttpStatus = dto.HttpStatus,
|
|
CreatedAtUtc = DateTime.SpecifyKind(dto.CreatedAtUtc.ToDateTime(), DateTimeKind.Utc),
|
|
UpdatedAtUtc = DateTime.SpecifyKind(dto.UpdatedAtUtc.ToDateTime(), DateTimeKind.Utc),
|
|
TerminalAtUtc = dto.TerminalAtUtc is null
|
|
? null
|
|
: DateTime.SpecifyKind(dto.TerminalAtUtc.ToDateTime(), DateTimeKind.Utc),
|
|
IngestedAtUtc = DateTime.UtcNow, // overwritten by AuditLogIngestActor
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Projects a site-local <see cref="SiteCallOperational"/> onto its
|
|
/// wire-format DTO for the Site Call Audit <c>PullSiteCalls</c>
|
|
/// reconciliation RPC. The inverse of <see cref="FromDto"/>; null
|
|
/// <see cref="SiteCallOperational.LastError"/> / <see cref="SiteCallOperational.SourceNode"/>
|
|
/// collapse to empty strings (proto3 scalar strings cannot be absent), while
|
|
/// the nullable <c>HttpStatus</c> and <c>TerminalAtUtc</c> stay unset on the
|
|
/// wire so true-null semantics survive the round-trip back through
|
|
/// <see cref="FromDto"/>.
|
|
/// </summary>
|
|
/// <param name="operational">The site-local operational state to project to wire format.</param>
|
|
/// <returns>A populated <see cref="SiteCallOperationalDto"/> ready for transmission.</returns>
|
|
public static SiteCallOperationalDto ToDto(SiteCallOperational operational)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(operational);
|
|
|
|
var dto = new SiteCallOperationalDto
|
|
{
|
|
TrackedOperationId = operational.TrackedOperationId.ToString(),
|
|
Channel = operational.Channel,
|
|
Target = operational.Target,
|
|
SourceSite = operational.SourceSite,
|
|
SourceNode = operational.SourceNode ?? string.Empty,
|
|
Status = operational.Status,
|
|
RetryCount = operational.RetryCount,
|
|
LastError = operational.LastError ?? string.Empty,
|
|
CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.CreatedAtUtc)),
|
|
UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.UpdatedAtUtc)),
|
|
};
|
|
|
|
if (operational.HttpStatus.HasValue)
|
|
{
|
|
dto.HttpStatus = operational.HttpStatus.Value;
|
|
}
|
|
|
|
if (operational.TerminalAtUtc.HasValue)
|
|
{
|
|
dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.TerminalAtUtc.Value));
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
// All ScadaBridge timestamps are UTC by invariant; Timestamp.FromDateTime
|
|
// requires UTC kind. Specify (never convert) so a row read back from SQLite
|
|
// with Kind=Utc passes through and a defensively-unspecified value is
|
|
// treated as the UTC it already is. Mirrors AuditEventDtoMapper.EnsureUtc.
|
|
private static DateTime EnsureUtc(DateTime value) =>
|
|
value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
|
}
|