docs: add XML doc comments across src + Sister Projects section in CLAUDE.md
Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public APIs across all 23 src/ projects so the doc-coverage gate is green. Also adds a Sister Projects section to CLAUDE.md pointing at the MxAccess Gateway and OtOpcUa sibling repos, and gitignores local credential captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
This commit is contained in:
@@ -28,20 +28,16 @@ public class AuditLogRepository : IAuditLogRepository
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
private readonly ILogger<AuditLogRepository> _logger;
|
||||
|
||||
/// <summary>Initializes a new instance of the AuditLogRepository class.</summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="logger">Optional logger instance.</param>
|
||||
public AuditLogRepository(ScadaLinkDbContext context, ILogger<AuditLogRepository>? logger = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? NullLogger<AuditLogRepository>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issues a single <c>IF NOT EXISTS … INSERT INTO dbo.AuditLog (…) VALUES (…)</c>
|
||||
/// via <see cref="Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolatedAsync"/>.
|
||||
/// Bypasses the EF change tracker so the row never enters a tracked state and
|
||||
/// the enum-as-string conversion is done explicitly in C# (the columns are
|
||||
/// declared <c>varchar(32)</c> via <c>HasConversion<string>()</c> in
|
||||
/// <see cref="ScadaLink.ConfigurationDatabase.Configurations.AuditLogEntityTypeConfiguration"/>).
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
if (evt is null)
|
||||
@@ -93,14 +89,7 @@ VALUES
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <c>AsNoTracking</c> queryable over <see cref="AuditEvent"/>, applies
|
||||
/// every non-null filter predicate, and pages by keyset on
|
||||
/// <c>(OccurredAtUtc DESC, EventId DESC)</c>. The keyset clause is expressed
|
||||
/// directly (<c>occurred < after || (occurred == after && eventId.CompareTo(afterId) < 0)</c>)
|
||||
/// — EF Core 10 translates <see cref="Guid.CompareTo(Guid)"/> against SQL Server's
|
||||
/// <c>uniqueidentifier</c> sort order.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AuditEvent>> QueryAsync(
|
||||
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default)
|
||||
{
|
||||
@@ -199,30 +188,7 @@ VALUES
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M6-T4 production implementation of the drop-and-rebuild dance documented
|
||||
/// on <see cref="IAuditLogRepository.SwitchOutPartitionAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The staging table name is GUID-suffixed so concurrent purge attempts on
|
||||
/// different boundaries cannot collide. The staging schema is byte-identical
|
||||
/// to the live <c>AuditLog</c> table (same column types, lengths,
|
||||
/// nullability, and clustered-key shape) — SQL Server's
|
||||
/// <c>ALTER TABLE … SWITCH PARTITION</c> rejects any drift. Keep this CREATE
|
||||
/// in sync with both the migration that ships the live table
|
||||
/// (<c>20260520142214_AddAuditLogTable</c>) and
|
||||
/// <c>AuditLogEntityTypeConfiguration</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All five steps run inside an explicit transaction so the SWITCH +
|
||||
/// staging-DROP are atomic from the perspective of a consumer reading via
|
||||
/// snapshot isolation; the CATCH rolls back and runs an idempotent
|
||||
/// "rebuild UX_AuditLog_EventId if it doesn't exist" so a partial failure
|
||||
/// never leaves the live table without its idempotency-supporting unique
|
||||
/// index.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default)
|
||||
{
|
||||
// GUID-suffixed staging name: prevents collision with any concurrent
|
||||
@@ -371,27 +337,7 @@ VALUES
|
||||
return rowsDeleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of <c>pf_AuditLog_Month</c> boundaries whose partition's
|
||||
/// <c>MAX(OccurredAtUtc)</c> is strictly older than <paramref name="threshold"/>.
|
||||
/// Boundaries with empty partitions are excluded — purging an empty
|
||||
/// partition is wasted I/O.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The CTE pulls every boundary value defined by the partition function and
|
||||
/// joins it (via <c>$PARTITION.pf_AuditLog_Month</c>) to the live AuditLog
|
||||
/// to compute per-partition <c>MAX(OccurredAtUtc)</c>. The outer filter
|
||||
/// keeps only those whose MAX is non-NULL (partition has rows) AND strictly
|
||||
/// less than the threshold (every row is past retention).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note: the query scans the live <c>OccurredAtUtc</c> column to compute
|
||||
/// the MAX per partition. With <c>IX_AuditLog_OccurredAtUtc</c> on the
|
||||
/// partition-aligned scheme this is a single index seek per partition; for
|
||||
/// 24 partitions and a daily purge cadence the cost is negligible.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
|
||||
DateTime threshold,
|
||||
CancellationToken ct = default)
|
||||
@@ -451,31 +397,7 @@ VALUES
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M7-T13 Bundle E — Health-dashboard Audit KPI tiles aggregate query.
|
||||
/// Single round-trip
|
||||
/// (<c>SELECT COUNT_BIG(*) AS Total, SUM(CASE WHEN Status IN (...) THEN 1 ELSE 0 END) AS Errors</c>)
|
||||
/// over the trailing <paramref name="window"/> anchored at
|
||||
/// <paramref name="nowUtc"/>. Returns a snapshot with
|
||||
/// <see cref="AuditLogKpiSnapshot.BacklogTotal"/> left at zero — the service
|
||||
/// layer composes that in from
|
||||
/// <see cref="ScadaLink.HealthMonitoring.ICentralHealthAggregator"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Why one query, not two: keeping the numerator + denominator in the same
|
||||
/// scan means the error rate the UI displays is computed from a consistent
|
||||
/// snapshot. With two separate queries a row could be inserted between
|
||||
/// them, inflating the denominator past the numerator (or vice-versa) and
|
||||
/// briefly producing a misleading percentage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "Error" rows are <c>Failed</c>, <c>Parked</c>, or <c>Discarded</c> — see
|
||||
/// <see cref="IAuditLogRepository.GetKpiSnapshotAsync"/> for the rationale.
|
||||
/// We pass the three discriminator strings as separate parameters rather
|
||||
/// than building an IN-list to keep the prepared statement cache-friendly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(
|
||||
TimeSpan window,
|
||||
DateTime? nowUtc = null,
|
||||
@@ -573,37 +495,7 @@ VALUES
|
||||
// practice.
|
||||
private const int ExecutionChainMaxDepth = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log ParentExecutionId (Task 8) — returns the whole execution chain
|
||||
/// containing <paramref name="executionId"/>, regardless of entry point.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Two phases. <b>Walk up:</b> an iterative
|
||||
/// <c>SELECT TOP 1 ParentExecutionId … WHERE ExecutionId = @cur AND ParentExecutionId IS NOT NULL</c>
|
||||
/// climbs from the supplied node to the root — the last execution id with no
|
||||
/// parent. The loop is capped at <see cref="ExecutionChainMaxDepth"/>
|
||||
/// iterations; a purged/missing parent simply ends the climb early. <b>Walk
|
||||
/// down:</b> a recursive CTE over a DISTINCT
|
||||
/// <c>(ExecutionId, ParentExecutionId)</c> edge set, seeded at the root edge
|
||||
/// and joining <c>edge.ParentExecutionId = chain.ExecutionId</c> to
|
||||
/// enumerate every descendant. Recursing over edges rather than raw rows
|
||||
/// keeps the recursion one path wide per execution. It is bounded by
|
||||
/// <c>OPTION (MAXRECURSION ...)</c> at <see cref="ExecutionChainMaxDepth"/>
|
||||
/// — corrupt cyclic data raises a <see cref="SqlException"/> (msg 530)
|
||||
/// rather than spinning.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The chain's full execution-id set is every edge's <c>ExecutionId</c>
|
||||
/// unioned with its non-null <c>ParentExecutionId</c>, so an execution
|
||||
/// referenced only as a parent — a "stub" that emitted no rows of its own,
|
||||
/// and therefore owns no edge of its own — is still included. The final
|
||||
/// projection LEFT JOINs that id set back to <c>AuditLog</c> and
|
||||
/// <c>GROUP BY</c>s, so a stub yields a node with <c>RowCount = 0</c> and
|
||||
/// empty/null aggregates. The query is SELECT-only
|
||||
/// (the audit writer role grants no UPDATE/DELETE — reads are unrestricted).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
|
||||
Guid executionId,
|
||||
CancellationToken ct = default)
|
||||
@@ -768,14 +660,7 @@ VALUES
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distinct non-null <c>SourceNode</c> values for the Audit Log page's
|
||||
/// Node filter dropdown. EF Core translates this to
|
||||
/// <c>SELECT DISTINCT SourceNode FROM AuditLog WHERE SourceNode IS NOT NULL ORDER BY SourceNode</c>
|
||||
/// — a single index-less scan, but the column is bounded (one entry per
|
||||
/// node in the cluster, currently <10) and the Central UI caches the
|
||||
/// result for ~60s, so a periodic scan is acceptable.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<AuditEvent>()
|
||||
|
||||
Reference in New Issue
Block a user