fix(persistence): drop broken Guid lookup on ISnapshotRepository (CRITICAL)

Snapshots are append-only and identified by the composite (EventId, CapturedAt),
not a surrogate Guid. The previous implementation inherited
IRepository<Guid, OddsSnapshot> and faked the lookup with (long)key.GetHashCode()
on a long auto-increment PK — collision-prone and non-portable. Nobody called
GetAsync(Guid) / DeleteAsync(Guid) anyway.

* ISnapshotRepository no longer extends IRepository<Guid, OddsSnapshot>; it
  exposes only the methods snapshots actually have: ListAsync, ListByEventAsync,
  AddAsync, SaveChangesAsync.
* SnapshotRepository drops the broken Get/Update/Delete methods.
This commit is contained in:
2026-05-09 15:13:22 +03:00
parent a627c360c3
commit a6bd8a0e44
2 changed files with 13 additions and 28 deletions
@@ -6,11 +6,23 @@ namespace Marathon.Application.Abstractions;
/// <summary>
/// Repository for <see cref="OddsSnapshot"/> domain entities.
/// </summary>
public interface ISnapshotRepository : IRepository<Guid, OddsSnapshot>
/// <remarks>
/// Snapshots are append-only and identified by the composite (EventId, CapturedAt)
/// rather than a surrogate key, so this contract intentionally does NOT extend
/// <see cref="IRepository{TKey, TEntity}"/> — point lookup by Guid would be
/// meaningless. Use <see cref="ListByEventAsync"/> for retrieval.
/// </remarks>
public interface ISnapshotRepository
{
Task<IReadOnlyList<OddsSnapshot>> ListAsync(CancellationToken ct = default);
Task<IReadOnlyList<OddsSnapshot>> ListByEventAsync(
EventId eventId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken ct = default);
Task AddAsync(OddsSnapshot entity, CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
@@ -11,18 +11,6 @@ internal sealed class SnapshotRepository : ISnapshotRepository
public SnapshotRepository(MarathonDbContext db) => _db = db;
public async Task<OddsSnapshot?> GetAsync(Guid key, CancellationToken ct = default)
{
var entity = await _db.Snapshots
.Include(s => s.Bets)
.FirstOrDefaultAsync(s => s.Id == (long)key.GetHashCode(), ct);
// Note: Guid→long mapping is lossy for GetAsync by Guid; the repo interface requires Guid key.
// Snapshots are typically retrieved by event, not directly by id.
// A proper implementation would store the Guid as a TEXT column.
// For now, this method is functionally available — callers prefer ListByEventAsync.
return entity is null ? null : Mapping.ToDomain(entity);
}
public async Task<IReadOnlyList<OddsSnapshot>> ListAsync(CancellationToken ct = default)
{
var entities = await _db.Snapshots.AsNoTracking()
@@ -56,21 +44,6 @@ internal sealed class SnapshotRepository : ISnapshotRepository
await _db.Snapshots.AddAsync(efEntity, ct);
}
public Task UpdateAsync(OddsSnapshot entity, CancellationToken ct = default)
{
// Snapshots are immutable once written — update is not a typical operation.
var efEntity = Mapping.ToEntity(entity);
_db.Snapshots.Update(efEntity);
return Task.CompletedTask;
}
public async Task DeleteAsync(Guid key, CancellationToken ct = default)
{
var entity = await _db.Snapshots.FindAsync([(long)key.GetHashCode()], ct);
if (entity is not null)
_db.Snapshots.Remove(entity);
}
public async Task SaveChangesAsync(CancellationToken ct = default) =>
await _db.SaveChangesAsync(ct);
}