using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; 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. /// /// /// /// All other members forward to the inner connection /// unchanged 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; 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). // 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) { _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; } /// /// 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; /// // ConnectionString is settable on DbConnection — forward both halves. public override string ConnectionString { // Some providers throw on get when the connection hasn't been opened // with a string set explicitly. The wrapper has no opinion — forward. #pragma warning disable CS8765 // nullability of overridden member parameter — base setter accepts null in practice get => _inner.ConnectionString; set => _inner.ConnectionString = value; #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) => _inner.ChangeDatabase(databaseName); /// 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) => _inner.BeginTransaction(isolationLevel); /// 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); } /// 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; } }