using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using AuditEvent = ZB.MOM.WW.Audit.AuditEvent; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// Thin decorator over the /// returned by /// . The decorator /// itself does no audit work — it simply intercepts /// so the handed back to /// the script is wrapped in an that emits one /// DbOutbound/DbWrite audit row per execution. /// /// /// /// Transactions and command connection readback retain this wrapper so they cannot /// expose an unaudited provider connection. Other members forward to the inner connection /// so the script keeps full ADO.NET semantics (transactions, state /// transitions, server-version queries, etc.). Disposing the wrapper disposes /// the inner connection — the caller is still responsible for disposal per /// the contract. /// /// /// The audit-write failure contract (alog.md §7) is honoured at the /// layer — see that class for the 3-layer /// fail-safe pattern (build, write, observe). /// /// internal sealed class AuditingDbConnection : DbConnection { private readonly DbConnection _inner; private readonly IAuditWriter _auditWriter; private readonly string _connectionName; private readonly string _siteId; private readonly string _instanceName; private readonly string? _sourceScript; private readonly Guid _executionId; /// /// ParentExecutionId: the spawning execution's id when this /// run was inbound-API-routed; null for non-routed runs. Threaded /// alongside into the /// so its DbWrite row stamps it. /// private readonly Guid? _parentExecutionId; /// /// Flow Debugger (plan 2026-09-06 D5): optional observer handed the SAME /// each gives the writer — /// the run's inline trace collector. Null when untraced. /// private readonly Action? _onAudit; private readonly ILogger _logger; /// /// Initializes a new auditing database connection decorator. /// /// The inner database connection to wrap and audit commands through. /// Writer for audit log entries. /// Name of the database connection being used. /// Identifier of the site executing commands. /// Unique name of the instance executing commands. /// Optional name of the source script for audit trail. /// Logger for diagnostics and warnings. /// Unique identifier for this script execution. /// Optional identifier of the parent execution (for routed calls). /// Optional observer of every emitted (the Flow Debugger trace collector). // Parameter ordering: executionId sits immediately after the ILogger, // consistent with the other three audit-threaded ctors (ExternalSystemHelper, // DatabaseHelper, AuditingDbCommand). parentExecutionId is a trailing // optional param so existing positional callers stay source-compatible. public AuditingDbConnection( DbConnection inner, IAuditWriter auditWriter, string connectionName, string siteId, string instanceName, string? sourceScript, ILogger logger, Guid executionId, Guid? parentExecutionId = null, Action? onAudit = null) { _inner = inner ?? throw new ArgumentNullException(nameof(inner)); _auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter)); _connectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName)); _siteId = siteId ?? string.Empty; _instanceName = instanceName ?? string.Empty; _sourceScript = sourceScript; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _executionId = executionId; _parentExecutionId = parentExecutionId; _onAudit = onAudit; } /// /// Exposes the wrapped to the /// sibling in the same assembly, so the /// command's DbConnection setter can unwrap an /// without reflecting into the /// private _inner field. Both classes are internal sealed /// in this assembly, so the accessor stays out of the public API and /// matches the precedent of preferring proper API surface /// over . /// internal DbConnection Inner => _inner; /// // The configured target is the audit identity; scripts cannot retarget it or // retrieve connection credentials through the raw ADO surface. public override string ConnectionString { #pragma warning disable CS8765 get => string.Empty; set => throw new NotSupportedException("Audited connections cannot be retargeted."); #pragma warning restore CS8765 } /// public override string Database => _inner.Database; /// public override string DataSource => _inner.DataSource; /// public override string ServerVersion => _inner.ServerVersion; /// public override ConnectionState State => _inner.State; /// public override void ChangeDatabase(string databaseName) => throw new NotSupportedException("Audited connections cannot be retargeted."); /// public override void Close() => _inner.Close(); /// public override void Open() => _inner.Open(); /// public override Task OpenAsync(CancellationToken cancellationToken) => _inner.OpenAsync(cancellationToken); /// protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => new AuditingDbTransaction(_inner.BeginTransaction(isolationLevel), this); /// protected override async ValueTask BeginDbTransactionAsync( IsolationLevel isolationLevel, CancellationToken cancellationToken) => new AuditingDbTransaction( await _inner.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false), this); /// protected override DbCommand CreateDbCommand() { var innerCmd = _inner.CreateCommand(); // Hand the script an auditing wrapper. The wrapper preserves the // inner command's identity for parameters / type maps via delegation. return new AuditingDbCommand( innerCmd, _auditWriter, _connectionName, _siteId, _instanceName, _sourceScript, _logger, _executionId, // ParentExecutionId: the spawning execution's id, // threaded alongside _executionId. Null for non-routed runs. _parentExecutionId, // Flow Debugger: the DbWrite rows ride the inline collector. _onAudit) { Connection = this }; } /// protected override void Dispose(bool disposing) { if (disposing) { _inner.Dispose(); } base.Dispose(disposing); } /// public override ValueTask DisposeAsync() { // DbConnection.DisposeAsync is virtual; calling base would run the // synchronous Dispose path. Forward to the inner connection // asynchronously and short-circuit the base. var task = _inner.DisposeAsync(); GC.SuppressFinalize(this); return task; } }