Files
MoneyMap/MoneyMap/Services/AccountService.cs
AJ Isaacs 53c674c6e0 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>
2025-11-24 21:10:56 -05:00

203 lines
5.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
namespace MoneyMap.Services;
/// <summary>
/// Service for account management including retrieval, validation, and deletion.
/// </summary>
public interface IAccountService
{
/// <summary>
/// Gets an account by ID with optional related data.
/// </summary>
Task<Account?> GetAccountByIdAsync(int id, bool includeRelated = false);
/// <summary>
/// Gets all accounts with optional statistics.
/// </summary>
Task<List<AccountWithStats>> GetAllAccountsWithStatsAsync();
/// <summary>
/// Gets account details with cards and transaction count.
/// </summary>
Task<AccountDetails?> GetAccountDetailsAsync(int id);
/// <summary>
/// Checks if an account can be deleted (no transactions exist).
/// </summary>
Task<DeleteValidationResult> CanDeleteAccountAsync(int id);
/// <summary>
/// Deletes an account if it has no associated transactions.
/// </summary>
Task<DeleteResult> DeleteAccountAsync(int id);
}
public class AccountService : IAccountService
{
private readonly MoneyMapContext _db;
public AccountService(MoneyMapContext db)
{
_db = db;
}
public async Task<Account?> GetAccountByIdAsync(int id, bool includeRelated = false)
{
var query = _db.Accounts.AsQueryable();
if (includeRelated)
{
query = query
.Include(a => a.Cards)
.Include(a => a.Transactions);
}
return await query.FirstOrDefaultAsync(a => a.Id == id);
}
public async Task<List<AccountWithStats>> GetAllAccountsWithStatsAsync()
{
var accounts = await _db.Accounts
.Include(a => a.Transactions)
.OrderBy(a => a.Owner)
.ThenBy(a => a.Institution)
.ThenBy(a => a.Last4)
.ToListAsync();
return accounts.Select(a => new AccountWithStats
{
Id = a.Id,
Institution = a.Institution,
AccountType = a.AccountType,
Last4 = a.Last4,
Owner = a.Owner,
Nickname = a.Nickname,
TransactionCount = a.Transactions.Count
}).ToList();
}
public async Task<AccountDetails?> GetAccountDetailsAsync(int id)
{
var account = await _db.Accounts.FindAsync(id);
if (account == null)
return null;
// 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();
// Get transaction count for this account
var accountTransactionCount = await _db.Transactions.CountAsync(t => t.AccountId == id);
return new AccountDetails
{
Account = account,
Cards = cardStats,
TransactionCount = accountTransactionCount
};
}
public async Task<DeleteValidationResult> CanDeleteAccountAsync(int id)
{
var account = await _db.Accounts
.Include(a => a.Transactions)
.FirstOrDefaultAsync(a => a.Id == id);
if (account == null)
return new DeleteValidationResult
{
CanDelete = false,
Reason = "Account not found."
};
if (account.Transactions.Any())
return new DeleteValidationResult
{
CanDelete = false,
Reason = $"Cannot delete account. It has {account.Transactions.Count} transaction(s) associated with it."
};
return new DeleteValidationResult { CanDelete = true };
}
public async Task<DeleteResult> DeleteAccountAsync(int id)
{
var validation = await CanDeleteAccountAsync(id);
if (!validation.CanDelete)
{
return new DeleteResult
{
Success = false,
Message = validation.Reason ?? "Cannot delete account."
};
}
var account = await _db.Accounts.FindAsync(id);
if (account == null)
{
return new DeleteResult
{
Success = false,
Message = "Account not found."
};
}
_db.Accounts.Remove(account);
await _db.SaveChangesAsync();
return new DeleteResult
{
Success = true,
Message = $"Deleted account {account.Institution} {account.Last4}"
};
}
}
// DTOs
public class AccountWithStats
{
public int Id { get; set; }
public string Institution { get; set; } = "";
public AccountType AccountType { get; set; }
public string Last4 { get; set; } = "";
public string Owner { get; set; } = "";
public string? Nickname { get; set; }
public int TransactionCount { get; set; }
}
public class AccountDetails
{
public Account Account { get; set; } = null!;
public List<CardWithStats> Cards { get; set; } = new();
public int TransactionCount { get; set; }
}
public class CardWithStats
{
public Card Card { get; set; } = null!;
public int TransactionCount { get; set; }
}
public class DeleteValidationResult
{
public bool CanDelete { get; set; }
public string? Reason { get; set; }
}
public class DeleteResult
{
public bool Success { get; set; }
public string Message { get; set; } = "";
}