using DodoSSH.Infrastructure; using Microsoft.EntityFrameworkCore; namespace DodoSSH.Api.Setup; /// /// Runs a write transaction under the retrying execution strategy so that a retry starts from the database /// rather than from what the failed attempt left in the change tracker. /// /// /// /// The strategy re-runs the delegate on the same context, and the context remembers the attempt that /// failed. When the failure is the commit itself — a connection dropped after SaveChanges had /// already succeeded — every entity that attempt wrote is marked unchanged, holding values the database /// rolled back. The retry then assigns the same values again, change detection sees nothing to save, and /// the transaction commits without them. Enrolment lost EnrolledAtUtc that way, and a team hand-over /// its two role changes, each reporting success. /// /// /// So a retry clears the tracker, and reloads each entity the delegate was handed from outside it — the ones /// it modifies or reads but did not load itself — so they are tracked again with what the database actually /// holds and the delegate's own assignments register as changes. The first attempt runs exactly as it did /// before: nothing is cleared or reloaded until something has failed. /// /// internal static class FreshAttempts { /// Runs under the context's execution strategy, freshly each time. /// The request's context. /// Entities loaded before the transaction that the attempt reads or modifies. /// The transaction, which begins and commits its own. internal static Task ExecuteFreshAsync( this DodoDbContext database, IReadOnlyList carried, Func> attempt) { ArgumentNullException.ThrowIfNull(database); ArgumentNullException.ThrowIfNull(carried); ArgumentNullException.ThrowIfNull(attempt); var strategy = database.Database.CreateExecutionStrategy(); var attempts = 0; return strategy.ExecuteAsync(async () => { if (attempts++ > 0) { database.ChangeTracker.Clear(); foreach (var entity in carried) { await database.Entry(entity).ReloadAsync().ConfigureAwait(false); } } return await attempt().ConfigureAwait(false); }); } /// internal static Task ExecuteFreshAsync( this DodoDbContext database, IReadOnlyList carried, Func attempt) => database.ExecuteFreshAsync( carried, async () => { await attempt().ConfigureAwait(false); return true; }); }