AVS-Carton-Shipment-Verifier/Services/SqliteScanHistoryStore.cs

235 lines
8.2 KiB
C#

using AVSCartonShipmentVerifier.DTOs;
using AVSCartonShipmentVerifier.Configuration;
using AVSCartonShipmentVerifier.ScanHistory;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace AVSCartonShipmentVerifier.Services;
public sealed class SqliteScanHistoryStore(
ScanHistoryDbContext dbContext,
IOptions<ScanHistoryOptions> options,
ILogger<SqliteScanHistoryStore> logger) : IScanHistoryStore
{
private readonly ScanHistoryDbContext _dbContext = dbContext;
private readonly ScanHistoryOptions _options = options.Value;
private readonly ILogger<SqliteScanHistoryStore> _logger = logger;
public async Task AddSuccessfulScanAsync(ScannedRecordDto record, CancellationToken cancellationToken = default)
{
try
{
_dbContext.ScanHistoryEntries.Add(new ScanHistoryEntry
{
ModelNumber = record.ModelNumber,
UniqueNumber = record.UniqueNumber,
ExistsInSystem = record.ExistsInSystem,
ShipmentMarked = record.ShipmentMarked,
ProcessedAtUtc = record.ProcessedAtUtc
});
await _dbContext.SaveChangesAsync(cancellationToken);
await TrimSuccessfulAsync(cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to persist successful scan history entry.");
}
}
public async Task AddRejectedScanAsync(RejectedScanRecordDto record, CancellationToken cancellationToken = default)
{
try
{
_dbContext.RejectedScanEntries.Add(new RejectedScanEntry
{
RawInputValue = record.RawInputValue,
ModelNumber = record.ModelNumber,
UniqueNumber = record.UniqueNumber,
RejectionReason = record.RejectionReason,
ProcessedAtUtc = record.ProcessedAtUtc
});
await _dbContext.SaveChangesAsync(cancellationToken);
await TrimRejectedAsync(cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to persist rejected scan history entry.");
}
}
public async Task<IReadOnlyCollection<ScannedRecordDto>> GetLastFiveSuccessfulScansAsync(CancellationToken cancellationToken = default)
{
try
{
var entries = await _dbContext.ScanHistoryEntries
.AsNoTracking()
.Where(x => x.ShipmentMarked)
.OrderByDescending(x => x.ProcessedAtUtc)
.Take(5)
.ToArrayAsync(cancellationToken);
return MapSuccessful(entries);
}
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Successful scan history (last five)"))
{
return Array.Empty<ScannedRecordDto>();
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read successful scan history.");
return Array.Empty<ScannedRecordDto>();
}
}
public async Task<IReadOnlyCollection<RejectedScanRecordDto>> GetLastFiveRejectedScansAsync(CancellationToken cancellationToken = default)
{
try
{
var entries = await _dbContext.RejectedScanEntries
.AsNoTracking()
.OrderByDescending(x => x.ProcessedAtUtc)
.Take(5)
.ToArrayAsync(cancellationToken);
return MapRejected(entries);
}
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Rejected scan history (last five)"))
{
return Array.Empty<RejectedScanRecordDto>();
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read rejected scan history.");
return Array.Empty<RejectedScanRecordDto>();
}
}
public async Task<IReadOnlyCollection<ScannedRecordDto>> GetAllSuccessfulScansAsync(CancellationToken cancellationToken = default)
{
try
{
var entries = await _dbContext.ScanHistoryEntries
.AsNoTracking()
.Where(x => x.ShipmentMarked)
.OrderByDescending(x => x.ProcessedAtUtc)
.ToArrayAsync(cancellationToken);
return MapSuccessful(entries);
}
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Successful scan history (all)"))
{
return Array.Empty<ScannedRecordDto>();
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read full successful scan history.");
return Array.Empty<ScannedRecordDto>();
}
}
public async Task<IReadOnlyCollection<RejectedScanRecordDto>> GetAllRejectedScansAsync(CancellationToken cancellationToken = default)
{
try
{
var entries = await _dbContext.RejectedScanEntries
.AsNoTracking()
.OrderByDescending(x => x.ProcessedAtUtc)
.ToArrayAsync(cancellationToken);
return MapRejected(entries);
}
catch (Exception exception) when (TryHandleReadCancellation(exception, cancellationToken, "Rejected scan history (all)"))
{
return Array.Empty<RejectedScanRecordDto>();
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read full rejected scan history.");
return Array.Empty<RejectedScanRecordDto>();
}
}
private bool TryHandleReadCancellation(Exception exception, CancellationToken cancellationToken, string operation)
{
if (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
{
return false;
}
_logger.LogDebug(exception, "{Operation} read was cancelled.", operation);
return true;
}
private static ScannedRecordDto[] MapSuccessful(IEnumerable<ScanHistoryEntry> entries) =>
entries
.Select(x => new ScannedRecordDto
{
ModelNumber = x.ModelNumber,
UniqueNumber = x.UniqueNumber,
ExistsInSystem = x.ExistsInSystem,
ShipmentMarked = x.ShipmentMarked,
ProcessedAtUtc = DateTime.SpecifyKind(x.ProcessedAtUtc, DateTimeKind.Utc)
})
.ToArray();
private static RejectedScanRecordDto[] MapRejected(IEnumerable<RejectedScanEntry> entries) =>
entries
.Select(x => new RejectedScanRecordDto
{
RawInputValue = x.RawInputValue,
ModelNumber = x.ModelNumber,
UniqueNumber = x.UniqueNumber,
RejectionReason = x.RejectionReason,
ProcessedAtUtc = DateTime.SpecifyKind(x.ProcessedAtUtc, DateTimeKind.Utc)
})
.ToArray();
private async Task TrimSuccessfulAsync(CancellationToken cancellationToken)
{
var max = _options.MaxRecordsToKeep;
if (max <= 0)
{
return;
}
var toDelete = await _dbContext.ScanHistoryEntries
.OrderByDescending(x => x.ProcessedAtUtc)
.Skip(max)
.Select(x => x.Id)
.ToArrayAsync(cancellationToken);
if (toDelete.Length == 0)
{
return;
}
_dbContext.ScanHistoryEntries.RemoveRange(toDelete.Select(id => new ScanHistoryEntry { Id = id }));
await _dbContext.SaveChangesAsync(cancellationToken);
}
private async Task TrimRejectedAsync(CancellationToken cancellationToken)
{
var max = _options.MaxRecordsToKeep;
if (max <= 0)
{
return;
}
var toDelete = await _dbContext.RejectedScanEntries
.OrderByDescending(x => x.ProcessedAtUtc)
.Skip(max)
.Select(x => x.Id)
.ToArrayAsync(cancellationToken);
if (toDelete.Length == 0)
{
return;
}
_dbContext.RejectedScanEntries.RemoveRange(toDelete.Select(id => new RejectedScanEntry { Id = id }));
await _dbContext.SaveChangesAsync(cancellationToken);
}
}