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; } }