9cff87fe85
Remove project bookkeeping citations from shipped code comments across the solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/ issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels, and C/D/K/S/T phase labels. Comment text only — no code logic, string/log literals, or XML-doc structure changed. Genuine descriptions are preserved (only the citation is stripped), and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365, UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker TaskReferenceInComment / TrackingReferenceInComment checks plus targeted grep passes; full solution builds clean, append-only guard tests pass.
201 lines
7.4 KiB
C#
201 lines
7.4 KiB
C#
using System.Collections;
|
|
using System.Data.Common;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
/// <summary>
|
|
/// <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 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);
|
|
}
|