Harden owner and suggestion invariants for concurrent writes

This commit is contained in:
2026-02-08 21:37:46 +01:00
parent 569cea161f
commit fe6a9d5da4
13 changed files with 472 additions and 22 deletions

View File

@@ -2,6 +2,7 @@ using GameList.Contracts;
using GameList.Data;
using GameList.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace GameList.Endpoints;
@@ -71,26 +72,46 @@ internal sealed class VoteWorkflowService(AppDbContext db)
.Where(v => v.PlayerId == playerId && linkedIds.Contains(v.SuggestionId))
.ToListAsync();
foreach (var linkedSuggestionId in linkedIds)
for (var attempt = 0; attempt < 2; attempt++)
{
var vote = existingVotes.FirstOrDefault(v => v.SuggestionId == linkedSuggestionId);
if (vote == null)
foreach (var linkedSuggestionId in linkedIds)
{
db.Votes.Add(new Vote
var vote = existingVotes.FirstOrDefault(v => v.SuggestionId == linkedSuggestionId);
if (vote == null)
{
PlayerId = playerId,
SuggestionId = linkedSuggestionId,
Score = score
});
db.Votes.Add(new Vote
{
PlayerId = playerId,
SuggestionId = linkedSuggestionId,
Score = score
});
}
else
{
vote.Score = score;
}
}
else
try
{
vote.Score = score;
await db.SaveChangesAsync();
return Results.Ok(new VoteUpsertResponse(linkedIds, score));
}
catch (DbUpdateException ex) when (attempt == 0 && EndpointHelpers.IsSqliteConstraintViolation(ex))
{
DetachAddedVotes(db.ChangeTracker.Entries<Vote>());
await db.Votes
.Where(v => v.PlayerId == playerId && linkedIds.Contains(v.SuggestionId))
.ExecuteUpdateAsync(v => v.SetProperty(x => x.Score, score));
existingVotes = await db.Votes
.Where(v => v.PlayerId == playerId && linkedIds.Contains(v.SuggestionId))
.ToListAsync();
}
}
await db.SaveChangesAsync();
return Results.Ok(new VoteUpsertResponse(linkedIds, score));
return EndpointHelpers.ConflictError("Vote update conflict. Please retry.");
}
public async Task<IResult> SetFinalizeAsync(Guid playerId, bool final)
@@ -105,4 +126,13 @@ internal sealed class VoteWorkflowService(AppDbContext db)
await db.SaveChangesAsync();
return Results.Ok(new VoteFinalizeResponse(player.VotesFinal));
}
private static void DetachAddedVotes(IEnumerable<EntityEntry<Vote>> voteEntries)
{
foreach (var entry in voteEntries)
{
if (entry.State == EntityState.Added)
entry.State = EntityState.Detached;
}
}
}