[High] Activity/knowledge query handlers load entire UserProfiles/UserActivities tables into memory #60

Closed
opened 2026-05-05 04:15:29 +00:00 by spikerj · 1 comment
Owner

Severity: High (perf/scalability)

Files:

  • SpikerSoft.Business/Domain/Activity/Queries/GetChildKnowledgeProgress/GetChildKnowledgeProgressQueryHandler.cs (~16) -- UserProfiles.ToListAsync() to find one user
  • SpikerSoft.Business/Domain/Activity/Queries/CompareKnowledgeProgress/CompareKnowledgeProgressQueryHandler.cs (~13) -- same
  • SpikerSoft.Business/Domain/Activity/Queries/GetActivityCoverage/GetActivityCoverageQueryHandler.cs (~13-16) -- UserActivities.ToListAsync() then in-memory filter

Problem: Currently fine for demo data, but at any meaningful user count this becomes O(N) per request and a memory pressure source.

Fix: Push filters into the LINQ query, use Where(...).FirstOrDefaultAsync() or Where(...).ToListAsync() after filtering, not before.

Acceptance criteria:

  • All three handlers query only the relevant rows
  • EF Core query plans verified (logged SQL)
  • No behavior regression in tests
**Severity:** High (perf/scalability) **Files:** - `SpikerSoft.Business/Domain/Activity/Queries/GetChildKnowledgeProgress/GetChildKnowledgeProgressQueryHandler.cs` (~16) -- `UserProfiles.ToListAsync()` to find one user - `SpikerSoft.Business/Domain/Activity/Queries/CompareKnowledgeProgress/CompareKnowledgeProgressQueryHandler.cs` (~13) -- same - `SpikerSoft.Business/Domain/Activity/Queries/GetActivityCoverage/GetActivityCoverageQueryHandler.cs` (~13-16) -- `UserActivities.ToListAsync()` then in-memory filter **Problem:** Currently fine for demo data, but at any meaningful user count this becomes O(N) per request and a memory pressure source. **Fix:** Push filters into the LINQ query, use `Where(...).FirstOrDefaultAsync()` or `Where(...).ToListAsync()` after filtering, not before. **Acceptance criteria:** - [ ] All three handlers query only the relevant rows - [ ] EF Core query plans verified (logged SQL) - [ ] No behavior regression in tests
Author
Owner

Resolved.

All three handlers now push filters into the LINQ query rather than loading full tables:

  • GetChildKnowledgeProgressQueryHandler -- replaced UserProfiles.ToListAsync() + in-memory FirstOrDefault with a single FirstOrDefaultAsync(p => p.KeycloakUserId == request.ChildUserId)
  • CompareKnowledgeProgressQueryHandler -- replaced full-table scan with Where(p => request.UserIds.Contains(p.KeycloakUserId)).ToListAsync(), then dictionary lookup for both auth gate and display-name lookup
  • GetActivityCoverageQueryHandler -- pushes the optional userId filter into the IQueryable before materialising

Files changed:

  • SpikerSoft.Business/Domain/Activity/Queries/GetChildKnowledgeProgress/GetChildKnowledgeProgressQueryHandler.cs
  • SpikerSoft.Business/Domain/Activity/Queries/CompareKnowledgeProgress/CompareKnowledgeProgressQueryHandler.cs
  • SpikerSoft.Business/Domain/Activity/Queries/GetActivityCoverage/GetActivityCoverageQueryHandler.cs
**Resolved.** All three handlers now push filters into the LINQ query rather than loading full tables: - `GetChildKnowledgeProgressQueryHandler` -- replaced `UserProfiles.ToListAsync()` + in-memory `FirstOrDefault` with a single `FirstOrDefaultAsync(p => p.KeycloakUserId == request.ChildUserId)` - `CompareKnowledgeProgressQueryHandler` -- replaced full-table scan with `Where(p => request.UserIds.Contains(p.KeycloakUserId)).ToListAsync()`, then dictionary lookup for both auth gate and display-name lookup - `GetActivityCoverageQueryHandler` -- pushes the optional `userId` filter into the `IQueryable` before materialising Files changed: - `SpikerSoft.Business/Domain/Activity/Queries/GetChildKnowledgeProgress/GetChildKnowledgeProgressQueryHandler.cs` - `SpikerSoft.Business/Domain/Activity/Queries/CompareKnowledgeProgress/CompareKnowledgeProgressQueryHandler.cs` - `SpikerSoft.Business/Domain/Activity/Queries/GetActivityCoverage/GetActivityCoverageQueryHandler.cs`
Sign in to join this conversation.