using FrameworkDAL.CustomCode.AuditQuery; using FrameworkDAL.CustomCode.ChangeTracking; using FrameworkDAL.DTO.AuditQuery; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace FrameworkBLL.AuditQuery { /// /// Read-only service for querying audit history. /// /// Archive fan-out: when predates the /// current hot-table retention boundary (UtcNow - ArchiveSettings.RetentionMonths), results /// are retrieved from both TEVENTLOG and TEVENTLOG_ARCHIVE and merged. /// /// Change detail: for GetByEntity and GetByUser calls, the BLL loads the associated /// TEVENTLOGCHANGE rows for each result and attaches them to the DTO. /// For SearchAudit (potentially large result sets) changes are NOT loaded by default /// to avoid N+1 — callers that need changes should call GetByEntity for individual records. /// public sealed class AuditQueryBLL : IAuditQueryBLL { private readonly IAuditQueryDAL _dal; private readonly IChangeTrackingDAL _changeDal; private readonly IOptions _archiveSettings; private readonly ILogger _logger; public AuditQueryBLL( IAuditQueryDAL dal, IChangeTrackingDAL changeDal, IOptions archiveSettings, ILogger logger) { _dal = dal; _changeDal = changeDal; _archiveSettings = archiveSettings; _logger = logger; } public async Task GetByEntityAsync( int entityType, int entityId, int pageOffset, int pageSize, LoginDTO login, CancellationToken ct = default) { var rows = await _dal.GetByEntityAsync(entityType, entityId, pageOffset, pageSize, login, ct) .ConfigureAwait(false); await EnrichWithChangesAsync(rows, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(rows); } public async Task GetByUserAsync( AuditSearchCriteriaDTO criteria, LoginDTO login, CancellationToken ct = default) { var rows = await _dal.GetByUserAsync(criteria, login, ct).ConfigureAwait(false); if (ShouldQueryArchive(criteria.DateFrom)) { var archiveRows = await _dal.GetByUserArchiveAsync(criteria, login, ct).ConfigureAwait(false); rows = MergeAndPage(rows, archiveRows, criteria.PageOffset, criteria.PageSize); } await EnrichWithChangesAsync(rows, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(rows); } public async Task SearchAsync( AuditSearchCriteriaDTO criteria, LoginDTO login, CancellationToken ct = default) { var rows = await _dal.SearchAsync(criteria, login, ct).ConfigureAwait(false); if (ShouldQueryArchive(criteria.DateFrom)) { var archiveRows = await _dal.SearchArchiveAsync(criteria, login, ct).ConfigureAwait(false); rows = MergeAndPage(rows, archiveRows, criteria.PageOffset, criteria.PageSize); } // Search results do not load per-row change detail — avoid N+1 on large result sets. // Callers needing changes should call GetByEntity for individual rows. return JsonSerializer.Serialize(rows); } // ── Helpers ─────────────────────────────────────────────────────────── private bool ShouldQueryArchive(DateTime? dateFrom) { if (dateFrom is null) return false; var boundary = DateTime.UtcNow.AddMonths(-_archiveSettings.Value.RetentionMonths); return dateFrom.Value < boundary; } private static List MergeAndPage( List hot, List archive, int pageOffset, int pageSize) { return hot.Concat(archive) .OrderByDescending(e => e.Timestamp) .Skip(pageOffset) .Take(pageSize) .ToList(); } private async Task EnrichWithChangesAsync( List rows, LoginDTO login, CancellationToken ct) { foreach (var row in rows) { try { row.Changes = await _changeDal .GetChangesByEventLogAsync(row.EventLogId, login, ct) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "AuditQueryBLL: failed to load changes for EventLogId={EventLogId}", row.EventLogId); // Leave Changes as empty list — do not abort the whole query } } } } /// /// Configuration for the audit archive boundary. /// Bound from appsettings section "ArchiveSettings". /// public sealed class ArchiveSettings { public const string Section = "ArchiveSettings"; /// How many months of events to keep in the hot TEVENTLOG table. Default: 6. public int RetentionMonths { get; set; } = 6; } }