using System.Data;
using System.Data.Common;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
///
/// 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 Guid _executionId;
///
/// ParentExecutionId: the spawning execution's id when this
/// run was inbound-API-routed; null for non-routed runs. Threaded
/// alongside into the
/// so its DbWrite row stamps it.
///
private readonly Guid? _parentExecutionId;
private readonly ILogger _logger;
///
/// Initializes a new auditing database connection decorator.
///
/// The inner database connection to wrap and audit commands through.
/// Writer for audit log entries.
/// Name of the database connection being used.
/// Identifier of the site executing commands.
/// Unique name of the instance executing commands.
/// Optional name of the source script for audit trail.
/// Logger for diagnostics and warnings.
/// Unique identifier for this script execution.
/// Optional identifier of the parent execution (for routed calls).
// Parameter ordering: executionId sits immediately after the ILogger,
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
// DatabaseHelper, AuditingDbCommand). parentExecutionId is a trailing
// optional param so existing positional callers stay source-compatible.
public AuditingDbConnection(
DbConnection inner,
IAuditWriter auditWriter,
string connectionName,
string siteId,
string instanceName,
string? sourceScript,
ILogger logger,
Guid executionId,
Guid? parentExecutionId = null)
{
_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));
_executionId = executionId;
_parentExecutionId = parentExecutionId;
}
///
/// Exposes the wrapped to the
/// sibling in the same assembly, so the
/// command's DbConnection setter can unwrap an
/// without reflecting into the
/// private _inner field. Both classes are internal sealed
/// in this assembly, so the accessor stays out of the public API and
/// matches the precedent of preferring proper API surface
/// over .
///
internal DbConnection Inner => _inner;
///
// 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,
_executionId,
// ParentExecutionId: the spawning execution's id,
// threaded alongside _executionId. Null for non-routed runs.
_parentExecutionId);
}
///
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;
}
}