Public Access
73 lines
2.9 KiB
C#
73 lines
2.9 KiB
C#
using DodoSSH.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace DodoSSH.Api.Setup;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>The strategy re-runs the delegate on the same context, and the context remembers the attempt that
|
|
/// failed.</b> When the failure is the commit itself — a connection dropped after <c>SaveChanges</c> 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 <c>EnrolledAtUtc</c> that way, and a team hand-over
|
|
/// its two role changes, each reporting success.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class FreshAttempts
|
|
{
|
|
/// <summary>Runs <paramref name="attempt"/> under the context's execution strategy, freshly each time.</summary>
|
|
/// <param name="database">The request's context.</param>
|
|
/// <param name="carried">Entities loaded before the transaction that the attempt reads or modifies.</param>
|
|
/// <param name="attempt">The transaction, which begins and commits its own.</param>
|
|
internal static Task<T> ExecuteFreshAsync<T>(
|
|
this DodoDbContext database,
|
|
IReadOnlyList<object> carried,
|
|
Func<Task<T>> 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);
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc cref="ExecuteFreshAsync{T}" />
|
|
internal static Task ExecuteFreshAsync(
|
|
this DodoDbContext database,
|
|
IReadOnlyList<object> carried,
|
|
Func<Task> attempt) =>
|
|
database.ExecuteFreshAsync(
|
|
carried,
|
|
async () =>
|
|
{
|
|
await attempt().ConfigureAwait(false);
|
|
return true;
|
|
});
|
|
}
|