perf(ui): batch event-list snapshot loads + push distinct dimensions to DB (HIGH)
* EventBrowsingService.BuildListAsync issued one snapshot query per event on every page render — N+1 against SQLite, with each round-trip hauling the full bet graph via Include(Bets). Replaced with a single ISnapshotRepository.ListByEventsAsync batch. * ListKnownSportCodesAsync / ListKnownCountryCodesAsync used to materialise every Event row to compute Distinct() in memory. Pushed to EF projection via two new IEventRepository methods (ListDistinctSportCodesAsync, ListDistinctCountryCodesAsync) implemented as .Select(...).Distinct().ToListAsync — single SELECT DISTINCT.
This commit is contained in:
@@ -12,4 +12,16 @@ public interface IEventRepository : IRepository<EventId, Event>
|
||||
Task<IReadOnlyList<Event>> ListByDateRangeAsync(DateRange range, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<Event>> ListBySportAsync(SportCode sport, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Distinct sport codes across the events table. Projects in the database
|
||||
/// rather than materialising every <see cref="Event"/> on the client.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<int>> ListDistinctSportCodesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Distinct ISO-2 country codes across the events table. Projects in the
|
||||
/// database rather than materialising every <see cref="Event"/>.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> ListDistinctCountryCodesAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,28 @@ internal sealed class EventRepository : IEventRepository
|
||||
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<int>> ListDistinctSportCodesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var codes = await _db.Events.AsNoTracking()
|
||||
.Select(e => e.SportCode)
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
|
||||
codes.Sort();
|
||||
return codes;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> 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);
|
||||
|
||||
@@ -68,25 +68,11 @@ public sealed class EventBrowsingService : IEventBrowsingService
|
||||
history);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<int>> 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<IReadOnlyList<int>> ListKnownSportCodesAsync(CancellationToken ct)
|
||||
=> _events.ListDistinctSportCodesAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<string>> 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<IReadOnlyList<string>> 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<EventListItem>();
|
||||
|
||||
// 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<EventListItem>(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<OddsSnapshot>();
|
||||
|
||||
var matching = snapshots
|
||||
.Where(s => s.Source == source)
|
||||
|
||||
Reference in New Issue
Block a user