diff --git a/src/Marathon.Application/Abstractions/IEventRepository.cs b/src/Marathon.Application/Abstractions/IEventRepository.cs index 8209476..fc99e10 100644 --- a/src/Marathon.Application/Abstractions/IEventRepository.cs +++ b/src/Marathon.Application/Abstractions/IEventRepository.cs @@ -12,4 +12,16 @@ public interface IEventRepository : IRepository Task> ListByDateRangeAsync(DateRange range, CancellationToken ct = default); Task> ListBySportAsync(SportCode sport, CancellationToken ct = default); + + /// + /// Distinct sport codes across the events table. Projects in the database + /// rather than materialising every on the client. + /// + Task> ListDistinctSportCodesAsync(CancellationToken ct = default); + + /// + /// Distinct ISO-2 country codes across the events table. Projects in the + /// database rather than materialising every . + /// + Task> ListDistinctCountryCodesAsync(CancellationToken ct = default); } diff --git a/src/Marathon.Infrastructure/Persistence/Repositories/EventRepository.cs b/src/Marathon.Infrastructure/Persistence/Repositories/EventRepository.cs index dfb6794..9167c10 100644 --- a/src/Marathon.Infrastructure/Persistence/Repositories/EventRepository.cs +++ b/src/Marathon.Infrastructure/Persistence/Repositories/EventRepository.cs @@ -50,6 +50,28 @@ internal sealed class EventRepository : IEventRepository return entities.Select(Mapping.ToDomain).ToList().AsReadOnly(); } + public async Task> ListDistinctSportCodesAsync(CancellationToken ct = default) + { + var codes = await _db.Events.AsNoTracking() + .Select(e => e.SportCode) + .Distinct() + .ToListAsync(ct); + + codes.Sort(); + return codes; + } + + public async Task> ListDistinctCountryCodesAsync(CancellationToken ct = default) + { + var codes = await _db.Events.AsNoTracking() + .Select(e => e.CountryCode) + .Distinct() + .ToListAsync(ct); + + codes.Sort(StringComparer.OrdinalIgnoreCase); + return codes; + } + public async Task AddAsync(Event entity, CancellationToken ct = default) { var efEntity = Mapping.ToEntity(entity); diff --git a/src/Marathon.UI/Services/EventBrowsingService.cs b/src/Marathon.UI/Services/EventBrowsingService.cs index 0780902..9e8579c 100644 --- a/src/Marathon.UI/Services/EventBrowsingService.cs +++ b/src/Marathon.UI/Services/EventBrowsingService.cs @@ -68,25 +68,11 @@ public sealed class EventBrowsingService : IEventBrowsingService history); } - public async Task> ListKnownSportCodesAsync(CancellationToken ct) - { - var all = await _events.ListAsync(ct).ConfigureAwait(false); - return all - .Select(static e => e.Sport.Value) - .Distinct() - .OrderBy(static x => x) - .ToList(); - } + public Task> ListKnownSportCodesAsync(CancellationToken ct) + => _events.ListDistinctSportCodesAsync(ct); - public async Task> ListKnownCountryCodesAsync(CancellationToken ct) - { - var all = await _events.ListAsync(ct).ConfigureAwait(false); - return all - .Select(static e => e.CountryCode) - .Distinct() - .OrderBy(static x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); - } + public Task> ListKnownCountryCodesAsync(CancellationToken ct) + => _events.ListDistinctCountryCodesAsync(ct); // ---------------- internals ---------------- @@ -121,18 +107,26 @@ public sealed class EventBrowsingService : IEventBrowsingService var sorted = ApplySort(filtered, filter.SortKey, filter.SortDescending); var materialized = sorted.ToList(); + if (materialized.Count == 0) + return Array.Empty(); - // Read each event's latest matching snapshot to populate the preview odds. + // Single batched snapshot query for all events on the page (replaces the + // prior per-event ListByEventAsync round-trip — N+1 against SQLite). The + // repository fills empty entries for events with no snapshots in range. var rangeFrom = filter.Dates.From.AddDays(-2); var rangeTo = filter.Dates.To.AddDays(2); + var ids = materialized.Select(e => e.Id).ToList(); + var snapshotsByEvent = await _snapshots + .ListByEventsAsync(ids, rangeFrom, rangeTo, ct) + .ConfigureAwait(false); var rows = new List(materialized.Count); foreach (var ev in materialized) { ct.ThrowIfCancellationRequested(); - var snapshots = await _snapshots - .ListByEventAsync(ev.Id, rangeFrom, rangeTo, ct) - .ConfigureAwait(false); + var snapshots = snapshotsByEvent.TryGetValue(ev.Id, out var found) + ? found + : Array.Empty(); var matching = snapshots .Where(s => s.Source == source)