using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.SiteRuntime.Scripts; /// /// Audit Log #23 — M4 Bundle A: 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 ILogger _logger; public AuditingDbConnection( DbConnection inner, IAuditWriter auditWriter, string connectionName, string siteId, string instanceName, string? sourceScript, ILogger logger) { _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)); } // 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); } 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; } }