Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/AuditingDbDataReader.cs
T
Joseph Doherty 7b0b9c7365 refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
2026-05-28 09:37:45 -04:00

201 lines
7.5 KiB
C#

using System.Collections;
using System.Data.Common;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <summary>
/// Audit Log #23 — M4 Bundle A: <see cref="DbDataReader"/> decorator that
/// counts the number of rows read by the script and fires a single audit
/// emission callback when the reader closes.
/// </summary>
/// <remarks>
/// <para>
/// The wrapping reader counts each successful <see cref="Read"/> /
/// <see cref="ReadAsync(CancellationToken)"/> and invokes <c>onClose</c>
/// exactly once — on <see cref="Close"/>, <see cref="CloseAsync"/>, or
/// disposal — with the running tally. This lets
/// <see cref="AuditingDbCommand"/> emit one
/// <c>DbOutbound</c>/<c>DbWrite</c> row per <c>ExecuteReader</c> with
/// <c>Extra.rowsReturned</c> populated, matching the M4 vocabulary lock.
/// </para>
/// <para>
/// Multiple result sets via <see cref="NextResult"/> are folded into a single
/// <c>rowsReturned</c> tally — the script sees one audit row per
/// <c>ExecuteReader</c> call, not per result set.
/// </para>
/// </remarks>
internal sealed class AuditingDbDataReader : DbDataReader
{
private readonly DbDataReader _inner;
private readonly Action<int> _onClose;
private int _rowsReturned;
private bool _closed;
/// <summary>
/// Initializes a new instance of the <see cref="AuditingDbDataReader"/> class, wrapping a data reader to count rows read.
/// </summary>
/// <param name="inner">The underlying DbDataReader to wrap and audit.</param>
/// <param name="onClose">Callback invoked once when the reader closes, receiving the total rows read.</param>
public AuditingDbDataReader(DbDataReader inner, Action<int> onClose)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_onClose = onClose ?? throw new ArgumentNullException(nameof(onClose));
}
// -- Row-count interception ------------------------------------------
/// <inheritdoc />
public override bool Read()
{
var more = _inner.Read();
if (more) _rowsReturned++;
return more;
}
/// <inheritdoc />
public override async Task<bool> ReadAsync(CancellationToken cancellationToken)
{
var more = await _inner.ReadAsync(cancellationToken).ConfigureAwait(false);
if (more) _rowsReturned++;
return more;
}
/// <inheritdoc />
public override void Close()
{
if (!_closed)
{
_closed = true;
try { _inner.Close(); }
finally { SafeFireOnClose(); }
}
}
/// <inheritdoc />
public override async Task CloseAsync()
{
if (!_closed)
{
_closed = true;
try { await _inner.CloseAsync().ConfigureAwait(false); }
finally { SafeFireOnClose(); }
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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 ------------------------------------------------
/// <inheritdoc />
public override object this[int ordinal] => _inner[ordinal];
/// <inheritdoc />
public override object this[string name] => _inner[name];
/// <inheritdoc />
public override int Depth => _inner.Depth;
/// <inheritdoc />
public override int FieldCount => _inner.FieldCount;
/// <inheritdoc />
public override bool HasRows => _inner.HasRows;
/// <inheritdoc />
public override bool IsClosed => _inner.IsClosed;
/// <inheritdoc />
public override int RecordsAffected => _inner.RecordsAffected;
/// <inheritdoc />
public override int VisibleFieldCount => _inner.VisibleFieldCount;
/// <inheritdoc />
public override bool GetBoolean(int ordinal) => _inner.GetBoolean(ordinal);
/// <inheritdoc />
public override byte GetByte(int ordinal) => _inner.GetByte(ordinal);
/// <inheritdoc />
public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
=> _inner.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
/// <inheritdoc />
public override char GetChar(int ordinal) => _inner.GetChar(ordinal);
/// <inheritdoc />
public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
=> _inner.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
/// <inheritdoc />
public override string GetDataTypeName(int ordinal) => _inner.GetDataTypeName(ordinal);
/// <inheritdoc />
public override DateTime GetDateTime(int ordinal) => _inner.GetDateTime(ordinal);
/// <inheritdoc />
public override decimal GetDecimal(int ordinal) => _inner.GetDecimal(ordinal);
/// <inheritdoc />
public override double GetDouble(int ordinal) => _inner.GetDouble(ordinal);
/// <inheritdoc />
public override IEnumerator GetEnumerator() => ((IEnumerable)_inner).GetEnumerator();
/// <inheritdoc />
public override Type GetFieldType(int ordinal) => _inner.GetFieldType(ordinal);
/// <inheritdoc />
public override float GetFloat(int ordinal) => _inner.GetFloat(ordinal);
/// <inheritdoc />
public override Guid GetGuid(int ordinal) => _inner.GetGuid(ordinal);
/// <inheritdoc />
public override short GetInt16(int ordinal) => _inner.GetInt16(ordinal);
/// <inheritdoc />
public override int GetInt32(int ordinal) => _inner.GetInt32(ordinal);
/// <inheritdoc />
public override long GetInt64(int ordinal) => _inner.GetInt64(ordinal);
/// <inheritdoc />
public override string GetName(int ordinal) => _inner.GetName(ordinal);
/// <inheritdoc />
public override int GetOrdinal(string name) => _inner.GetOrdinal(name);
/// <inheritdoc />
public override string GetString(int ordinal) => _inner.GetString(ordinal);
/// <inheritdoc />
public override object GetValue(int ordinal) => _inner.GetValue(ordinal);
/// <inheritdoc />
public override int GetValues(object[] values) => _inner.GetValues(values);
/// <inheritdoc />
public override bool IsDBNull(int ordinal) => _inner.IsDBNull(ordinal);
/// <inheritdoc />
public override bool NextResult() => _inner.NextResult();
/// <inheritdoc />
public override Task<bool> NextResultAsync(CancellationToken cancellationToken) => _inner.NextResultAsync(cancellationToken);
}