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>> ListByDateRangeAsync(DateRange range, CancellationToken ct = default);
|
||||||
|
|
||||||
Task<IReadOnlyList<Event>> ListBySportAsync(SportCode sport, 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();
|
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)
|
public async Task AddAsync(Event entity, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var efEntity = Mapping.ToEntity(entity);
|
var efEntity = Mapping.ToEntity(entity);
|
||||||
|
|||||||
@@ -68,25 +68,11 @@ public sealed class EventBrowsingService : IEventBrowsingService
|
|||||||
history);
|
history);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<int>> ListKnownSportCodesAsync(CancellationToken ct)
|
public Task<IReadOnlyList<int>> ListKnownSportCodesAsync(CancellationToken ct)
|
||||||
{
|
=> _events.ListDistinctSportCodesAsync(ct);
|
||||||
var all = await _events.ListAsync(ct).ConfigureAwait(false);
|
|
||||||
return all
|
|
||||||
.Select(static e => e.Sport.Value)
|
|
||||||
.Distinct()
|
|
||||||
.OrderBy(static x => x)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<string>> ListKnownCountryCodesAsync(CancellationToken ct)
|
public Task<IReadOnlyList<string>> ListKnownCountryCodesAsync(CancellationToken ct)
|
||||||
{
|
=> _events.ListDistinctCountryCodesAsync(ct);
|
||||||
var all = await _events.ListAsync(ct).ConfigureAwait(false);
|
|
||||||
return all
|
|
||||||
.Select(static e => e.CountryCode)
|
|
||||||
.Distinct()
|
|
||||||
.OrderBy(static x => x, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- internals ----------------
|
// ---------------- internals ----------------
|
||||||
|
|
||||||
@@ -121,18 +107,26 @@ public sealed class EventBrowsingService : IEventBrowsingService
|
|||||||
|
|
||||||
var sorted = ApplySort(filtered, filter.SortKey, filter.SortDescending);
|
var sorted = ApplySort(filtered, filter.SortKey, filter.SortDescending);
|
||||||
var materialized = sorted.ToList();
|
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 rangeFrom = filter.Dates.From.AddDays(-2);
|
||||||
var rangeTo = filter.Dates.To.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);
|
var rows = new List<EventListItem>(materialized.Count);
|
||||||
foreach (var ev in materialized)
|
foreach (var ev in materialized)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var snapshots = await _snapshots
|
var snapshots = snapshotsByEvent.TryGetValue(ev.Id, out var found)
|
||||||
.ListByEventAsync(ev.Id, rangeFrom, rangeTo, ct)
|
? found
|
||||||
.ConfigureAwait(false);
|
: Array.Empty<OddsSnapshot>();
|
||||||
|
|
||||||
var matching = snapshots
|
var matching = snapshots
|
||||||
.Where(s => s.Source == source)
|
.Where(s => s.Source == source)
|
||||||
|
|||||||
Reference in New Issue
Block a user