fixes: MRN generation race condition and Sepsis bundle creation race condition (TOCTOU)

This commit is contained in:
voltsrage
2026-06-21 16:37:22 +08:00
parent 4583bd7f44
commit 076e8364b2
8 changed files with 2635 additions and 7 deletions
@@ -1,29 +1,32 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
public class PatientService : IPatientService
{
private readonly AppDbContext _db;
private readonly IExternalIdentifierService _identifiers;
private readonly IAuditService _audit;
private readonly PatientOptions _options;
public PatientService(
AppDbContext db,
IExternalIdentifierService identifiers,
IAuditService audit)
IAuditService audit,
IOptions<PatientOptions> options)
{
_db = db;
_identifiers = identifiers;
_audit = audit;
_options = options.Value;
}
public async Task<Patient> RegisterAsync(RegisterPatientRequest req)
{
var mrn = await GenerateMrnAsync();
var patient = new Patient
{
Id = Guid.NewGuid(),
Mrn = mrn,
Mrn = await GenerateMrnAsync(),
FirstName = req.FirstName,
LastName = req.LastName,
DateOfBirth = req.DateOfBirth,
@@ -35,7 +38,18 @@ public class PatientService : IPatientService
CreatedAt = DateTimeOffset.UtcNow
};
_db.Patients.Add(patient);
await _db.SaveChangesAsync();
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateException ex) when (IsMrnUniqueViolation(ex))
{
_db.Entry(patient).State = EntityState.Detached;
patient.Mrn = await GenerateMrnAsync();
_db.Patients.Add(patient);
await _db.SaveChangesAsync();
}
await _audit.WriteAsync(
AuditAction.PatientRegistered,
@@ -46,6 +60,13 @@ public class PatientService : IPatientService
return patient;
}
private static bool IsMrnUniqueViolation(DbUpdateException ex)
{
return ex.InnerException is Npgsql.PostgresException pg
&& pg.SqlState == "23505"
&& pg.ConstraintName?.Contains("mrn") == true;
}
public async Task<PagedResult<Patient>> ListAsync(
string? q, int page, int pageSize)
{
@@ -196,7 +217,9 @@ public class PatientService : IPatientService
private async Task<string> GenerateMrnAsync()
{
var count = await _db.Patients.CountAsync();
return $"MRN-{(count + 1):D6}";
var seq = await _db.Database
.SqlQueryRaw<long>("SELECT nextval('mrn_seq') AS \"Value\"")
.SingleAsync();
return $"{_options.MrnPrefix}-{seq.ToString($"D{_options.MrnDigits}")}";
}
}