fixes: MRN generation race condition and Sepsis bundle creation race condition (TOCTOU)
This commit is contained in:
@@ -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}")}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user