using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.SecuredWrites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
///
/// EF Core implementation of over the central
/// PendingSecuredWrites table (M7 OPC UA / MxGateway UX, Task T14b). Mirrors the
/// SiteCallAuditRepository data-access shape: plain tracked EF reads/writes
/// against the shared , no raw SQL needed.
///
public class SecuredWriteRepository : ISecuredWriteRepository
{
private readonly ScadaBridgeDbContext _context;
///
/// Initializes a new instance of the class.
///
/// The EF Core database context.
public SecuredWriteRepository(ScadaBridgeDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
///
public async Task AddAsync(PendingSecuredWrite securedWrite, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(securedWrite);
await _context.Set().AddAsync(securedWrite, ct);
await _context.SaveChangesAsync(ct);
return securedWrite.Id;
}
///
public async Task GetAsync(long id, CancellationToken ct = default)
{
return await _context.Set()
.FindAsync(new object?[] { id }, ct);
}
///
public async Task UpdateAsync(PendingSecuredWrite securedWrite, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(securedWrite);
// The caller hands back the same tracked instance it read; Update covers the
// detached case too (re-attaches and marks every column modified).
_context.Set().Update(securedWrite);
await _context.SaveChangesAsync(ct);
}
///
public async Task> QueryAsync(
string? status,
string? siteId,
int skip,
int take,
CancellationToken ct = default)
{
IQueryable query = _context.Set()
.AsNoTracking();
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(p => p.Status == status);
}
if (!string.IsNullOrWhiteSpace(siteId))
{
query = query.Where(p => p.SiteId == siteId);
}
return await query
.OrderByDescending(p => p.SubmittedAtUtc)
.ThenByDescending(p => p.Id)
.Skip(skip)
.Take(take)
.ToListAsync(ct);
}
///
public async Task TryMarkApprovedAsync(
long id,
string verifierUser,
string? verifierComment,
DateTime decidedAtUtc,
CancellationToken ct = default)
{
// Single-statement compare-and-swap: the conditional WHERE Status='Pending'
// makes the Pending->Approved transition atomic at the row level, so two
// verifiers approving concurrently produce exactly one rowsAffected==1 (the
// winner) and one rowsAffected==0 (the loser). Parameterised via
// ExecuteSqlInterpolatedAsync — same raw-SQL conditional-update pattern as
// SiteCallAuditRepository's upsert-on-newer-status path.
var rowsAffected = await _context.Database.ExecuteSqlInterpolatedAsync(
$@"UPDATE dbo.PendingSecuredWrites
SET Status = 'Approved',
VerifierUser = {verifierUser},
VerifierComment = {verifierComment},
DecidedAtUtc = {decidedAtUtc}
WHERE Id = {id}
AND Status = 'Pending';",
ct);
return rowsAffected == 1;
}
}