Perf: Fix N+1 queries in entity services

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-11-24 21:10:56 -05:00
parent a85de793d7
commit 53c674c6e0
3 changed files with 33 additions and 51 deletions

View File

@@ -85,24 +85,18 @@ 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)
.Select(c => new CardWithStats
{
Card = c,
TransactionCount = c.Transactions.Count
})
.ToListAsync();
var cardStats = new List<CardWithStats>();
foreach (var card in cards)
{
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
cardStats.Add(new CardWithStats
{
Card = card,
TransactionCount = transactionCount
});
}
// Get transaction count for this account
var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id);

View File

@@ -55,26 +55,17 @@ public class CardService : ICardService
public async Task<List<CardWithStats>> 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)
.Select(c => new CardWithStats
{
Card = c,
TransactionCount = c.Transactions.Count
})
.ToListAsync();
var cardStats = new List<CardWithStats>();
foreach (var card in cards)
{
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
cardStats.Add(new CardWithStats
{
Card = card,
TransactionCount = transactionCount
});
}
return cardStats;
}
public async Task<DeleteValidationResult> CanDeleteCardAsync(int id)

View File

@@ -36,15 +36,19 @@ public class TransactionStatisticsService : ITransactionStatisticsService
public async Task<TransactionStats> CalculateStatsAsync(IQueryable<Transaction> query)
{
var allFilteredTransactions = await query.ToListAsync();
return new TransactionStats
// 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 = 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)
};
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 stats ?? new TransactionStats();
}
public async Task<CategorizationStats> GetCategorizationStatsAsync()
@@ -64,24 +68,17 @@ public class TransactionStatisticsService : ITransactionStatisticsService
public async Task<List<CardStats>> 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)
.Select(c => new CardStats
{
Card = c,
TransactionCount = c.Transactions.Count
})
.ToListAsync();
var cardStats = new List<CardStats>();
foreach (var card in cards)
{
var transactionCount = await _db.Transactions.CountAsync(t => t.CardId == card.Id);
cardStats.Add(new CardStats
{
Card = card,
TransactionCount = transactionCount
});
}
return cardStats;
}
}