From 53c674c6e0d14728f2c4b2186894ec77ae3c9a99 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 24 Nov 2025 21:10:56 -0500 Subject: [PATCH] Perf: Fix N+1 queries in entity services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardService.GetAllCardsWithStatsAsync: Use single query with Select projection - AccountService.GetAccountDetailsAsync: Use single query with Select projection - TransactionStatisticsService: Calculate stats at DB level, fix N+1 in GetCardStatsForAccountAsync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MoneyMap/Services/AccountService.cs | 20 ++++----- MoneyMap/Services/CardService.cs | 23 ++++------- .../Services/TransactionStatisticsService.cs | 41 +++++++++---------- 3 files changed, 33 insertions(+), 51 deletions(-) diff --git a/MoneyMap/Services/AccountService.cs b/MoneyMap/Services/AccountService.cs index 98147b8..c045f6c 100644 --- a/MoneyMap/Services/AccountService.cs +++ b/MoneyMap/Services/AccountService.cs @@ -85,23 +85,17 @@ public class AccountService : IAccountService if (account == null) return null; - // Get cards linked to this account - var cards = await _db.Cards + // Single query with projection to avoid N+1 + var cardStats = await _db.Cards .Where(c => c.AccountId == id) .OrderBy(c => c.Owner) .ThenBy(c => c.Last4) - .ToListAsync(); - - var cardStats = new List(); - foreach (var card in cards) - { - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - cardStats.Add(new CardWithStats + .Select(c => new CardWithStats { - Card = card, - TransactionCount = transactionCount - }); - } + Card = c, + TransactionCount = c.Transactions.Count + }) + .ToListAsync(); // Get transaction count for this account var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id); diff --git a/MoneyMap/Services/CardService.cs b/MoneyMap/Services/CardService.cs index 9ef6195..76eea26 100644 --- a/MoneyMap/Services/CardService.cs +++ b/MoneyMap/Services/CardService.cs @@ -55,26 +55,17 @@ public class CardService : ICardService public async Task> GetAllCardsWithStatsAsync() { - var cards = await _db.Cards + // Single query with projection to avoid N+1 + return await _db.Cards .Include(c => c.Account) .OrderBy(c => c.Owner) .ThenBy(c => c.Last4) - .ToListAsync(); - - var cardStats = new List(); - - foreach (var card in cards) - { - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - - cardStats.Add(new CardWithStats + .Select(c => new CardWithStats { - Card = card, - TransactionCount = transactionCount - }); - } - - return cardStats; + Card = c, + TransactionCount = c.Transactions.Count + }) + .ToListAsync(); } public async Task CanDeleteCardAsync(int id) diff --git a/MoneyMap/Services/TransactionStatisticsService.cs b/MoneyMap/Services/TransactionStatisticsService.cs index aebe0bf..6fb5816 100644 --- a/MoneyMap/Services/TransactionStatisticsService.cs +++ b/MoneyMap/Services/TransactionStatisticsService.cs @@ -36,15 +36,19 @@ public class TransactionStatisticsService : ITransactionStatisticsService public async Task CalculateStatsAsync(IQueryable query) { - var allFilteredTransactions = await query.ToListAsync(); + // Calculate stats at database level instead of loading all transactions into memory + var stats = await query + .GroupBy(_ => 1) // Group all into one group to aggregate + .Select(g => new TransactionStats + { + Count = g.Count(), + TotalDebits = g.Where(t => t.Amount < 0).Sum(t => t.Amount), + TotalCredits = g.Where(t => t.Amount > 0).Sum(t => t.Amount), + NetAmount = g.Sum(t => t.Amount) + }) + .FirstOrDefaultAsync(); - return new TransactionStats - { - Count = allFilteredTransactions.Count, - TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount), - TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount), - NetAmount = allFilteredTransactions.Sum(t => t.Amount) - }; + return stats ?? new TransactionStats(); } public async Task GetCategorizationStatsAsync() @@ -64,24 +68,17 @@ public class TransactionStatisticsService : ITransactionStatisticsService public async Task> GetCardStatsForAccountAsync(int accountId) { - var cards = await _db.Cards + // Single query with projection to avoid N+1 + return await _db.Cards .Where(c => c.AccountId == accountId) .OrderBy(c => c.Owner) .ThenBy(c => c.Last4) - .ToListAsync(); - - var cardStats = new List(); - foreach (var card in cards) - { - var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id); - cardStats.Add(new CardStats + .Select(c => new CardStats { - Card = card, - TransactionCount = transactionCount - }); - } - - return cardStats; + Card = c, + TransactionCount = c.Transactions.Count + }) + .ToListAsync(); } }