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);
}