using System.Collections; using System.Data.Common; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// Audit Log #23 — M4 Bundle A: decorator that /// counts the number of rows read by the script and fires a single audit /// emission callback when the reader closes. /// /// /// /// The wrapping reader counts each successful / /// and invokes onClose /// exactly once — on , , or /// disposal — with the running tally. This lets /// emit one /// DbOutbound/DbWrite row per ExecuteReader with /// Extra.rowsReturned populated, matching the M4 vocabulary lock. /// /// /// Multiple result sets via are folded into a single /// rowsReturned tally — the script sees one audit row per /// ExecuteReader call, not per result set. /// /// internal sealed class AuditingDbDataReader : DbDataReader { private readonly DbDataReader _inner; private readonly Action _onClose; private int _rowsReturned; private bool _closed; /// /// Initializes a new instance of the class, wrapping a data reader to count rows read. /// /// The underlying DbDataReader to wrap and audit. /// Callback invoked once when the reader closes, receiving the total rows read. public AuditingDbDataReader(DbDataReader inner, Action onClose) { _inner = inner ?? throw new ArgumentNullException(nameof(inner)); _onClose = onClose ?? throw new ArgumentNullException(nameof(onClose)); } // -- Row-count interception ------------------------------------------ /// public override bool Read() { var more = _inner.Read(); if (more) _rowsReturned++; return more; } /// public override async Task ReadAsync(CancellationToken cancellationToken) { var more = await _inner.ReadAsync(cancellationToken).ConfigureAwait(false); if (more) _rowsReturned++; return more; } /// public override void Close() { if (!_closed) { _closed = true; try { _inner.Close(); } finally { SafeFireOnClose(); } } } /// public override async Task CloseAsync() { if (!_closed) { _closed = true; try { await _inner.CloseAsync().ConfigureAwait(false); } finally { SafeFireOnClose(); } } } /// protected override void Dispose(bool disposing) { if (disposing) { // DbDataReader.Dispose calls Close on most providers, but we // guard with _closed to ensure onClose fires exactly once. if (!_closed) { _closed = true; try { _inner.Dispose(); } finally { SafeFireOnClose(); } } else { _inner.Dispose(); } } base.Dispose(disposing); } /// public override async ValueTask DisposeAsync() { if (!_closed) { _closed = true; try { await _inner.DisposeAsync().ConfigureAwait(false); } finally { SafeFireOnClose(); } } else { await _inner.DisposeAsync().ConfigureAwait(false); } GC.SuppressFinalize(this); } private void SafeFireOnClose() { // The onClose callback runs the audit emission, which is itself // best-effort and swallows internally — but defend the reader's own // close path anyway so an audit fault never propagates out of // Close/Dispose. try { _onClose(_rowsReturned); } catch { /* audit emission is best-effort by contract */ } } // -- Forwarded surface ------------------------------------------------ /// public override object this[int ordinal] => _inner[ordinal]; /// public override object this[string name] => _inner[name]; /// public override int Depth => _inner.Depth; /// public override int FieldCount => _inner.FieldCount; /// public override bool HasRows => _inner.HasRows; /// public override bool IsClosed => _inner.IsClosed; /// public override int RecordsAffected => _inner.RecordsAffected; /// public override int VisibleFieldCount => _inner.VisibleFieldCount; /// public override bool GetBoolean(int ordinal) => _inner.GetBoolean(ordinal); /// public override byte GetByte(int ordinal) => _inner.GetByte(ordinal); /// public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => _inner.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); /// public override char GetChar(int ordinal) => _inner.GetChar(ordinal); /// public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => _inner.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); /// public override string GetDataTypeName(int ordinal) => _inner.GetDataTypeName(ordinal); /// public override DateTime GetDateTime(int ordinal) => _inner.GetDateTime(ordinal); /// public override decimal GetDecimal(int ordinal) => _inner.GetDecimal(ordinal); /// public override double GetDouble(int ordinal) => _inner.GetDouble(ordinal); /// public override IEnumerator GetEnumerator() => ((IEnumerable)_inner).GetEnumerator(); /// public override Type GetFieldType(int ordinal) => _inner.GetFieldType(ordinal); /// public override float GetFloat(int ordinal) => _inner.GetFloat(ordinal); /// public override Guid GetGuid(int ordinal) => _inner.GetGuid(ordinal); /// public override short GetInt16(int ordinal) => _inner.GetInt16(ordinal); /// public override int GetInt32(int ordinal) => _inner.GetInt32(ordinal); /// public override long GetInt64(int ordinal) => _inner.GetInt64(ordinal); /// public override string GetName(int ordinal) => _inner.GetName(ordinal); /// public override int GetOrdinal(string name) => _inner.GetOrdinal(name); /// public override string GetString(int ordinal) => _inner.GetString(ordinal); /// public override object GetValue(int ordinal) => _inner.GetValue(ordinal); /// public override int GetValues(object[] values) => _inner.GetValues(values); /// public override bool IsDBNull(int ordinal) => _inner.IsDBNull(ordinal); /// public override bool NextResult() => _inner.NextResult(); /// public override Task NextResultAsync(CancellationToken cancellationToken) => _inner.NextResultAsync(cancellationToken); }