PR F: precompute dictionaries in ViewService
Eliminate O(N*M) linear scans in view assembly. GetTransactionViewList / GetBookingRuleViewList: precompute accountsById and use dictionary lookups instead of accountList.Single per row. GetAccountViewList: group transactions by TargetAccountId/OriginAccountId once; per-account balance is now O(k) instead of a full scan. Fold GetBalanceInAccountCurrency and GetCurrentBalanceInCHF into a single ComputeBalance helper so the account-currency balance is computed once per account instead of twice. Defer item 19 (async I/O): after PR D each XML file is loaded at most once per circuit, and this app is single-user local Blazor Server, so async would be ~40 files of churn for negligible benefit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2eedd2fd20
commit
5d75bbb64d
2 changed files with 55 additions and 65 deletions
|
|
@ -100,5 +100,6 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full
|
||||||
- **PR C** — Phase 2 mechanical cleanup: items 11, 13, 15, 16, 17.
|
- **PR C** — Phase 2 mechanical cleanup: items 11, 13, 15, 16, 17.
|
||||||
- **PR D** — Phase 2 item 9: per-request unit of work / cache. Split off from PR C because it is invasive enough to warrant its own review.
|
- **PR D** — Phase 2 item 9: per-request unit of work / cache. Split off from PR C because it is invasive enough to warrant its own review.
|
||||||
- **PR E** — Phase 3 item 22: ChartService cleanup. Promoted ahead of async because it fixes an active correctness bug — `GetAccountExpensesPerMonth(accountId, year)` was mutating `ISettingsService` on the singleton to hop years, with the year parameter always hardcoded to `DateTime.Now.Year`, so the Spendings-over-time chart discarded the user's header year selection.
|
- **PR E** — Phase 3 item 22: ChartService cleanup. Promoted ahead of async because it fixes an active correctness bug — `GetAccountExpensesPerMonth(accountId, year)` was mutating `ISettingsService` on the singleton to hop years, with the year parameter always hardcoded to `DateTime.Now.Year`, so the Spendings-over-time chart discarded the user's header year selection.
|
||||||
- **PR F** — Phase 3 items 19 + 21: async I/O across the repository stack, and precomputed dictionaries for view assembly.
|
- **PR F** — Phase 3 item 21: precomputed dictionaries in `ViewService` to eliminate O(N·M) `Single(...)` scans and per-account transaction filtering.
|
||||||
- **PR G** — Phase 4 (tests) — locks in behavior for PR F.
|
- **Item 19 (async I/O)** — deferred. After PR D, each XML file is loaded at most once per SignalR circuit, and this app is single-user local Blazor Server. Converting every repository/service method to async would touch ~40 files for negligible user-visible benefit and real regression risk. Revisit if the app is ever hosted for multiple concurrent users.
|
||||||
|
- **PR G** — Phase 4 (tests) — locks in behavior before further invasive changes.
|
||||||
|
|
|
||||||
|
|
@ -57,22 +57,43 @@ namespace Schaad.Accounting.Services
|
||||||
var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name);
|
var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name);
|
||||||
var settings = settingsService.GetSettings();
|
var settings = settingsService.GetSettings();
|
||||||
|
|
||||||
var accountList = accounts.Select(
|
// Group transactions by account so per-account balance is O(k) instead of O(M).
|
||||||
a =>
|
var creditsByAccount = transactionList.GroupBy(t => t.TargetAccountId).ToDictionary(g => g.Key, g => g.ToList());
|
||||||
new AccountDataset(
|
var debitsByAccount = transactionList.GroupBy(t => t.OriginAccountId).ToDictionary(g => g.Key, g => g.ToList());
|
||||||
account: a,
|
|
||||||
balance: GetBalanceInAccountCurrency(a, transactionList),
|
var accountList = accounts.Select(a =>
|
||||||
balanceCHF: GetCurrentBalanceInCHF(a, transactionList),
|
{
|
||||||
startBalanceCHF: fxService.ConvertCurrency(a.StartBalance, a.Currency, "CHF", settings.FixerIoApiKey),
|
var balance = ComputeBalance(a, creditsByAccount, debitsByAccount);
|
||||||
className: subclassRepository.GetClass(a.Class),
|
return new AccountDataset(
|
||||||
subClassName: subClassNameByNumber[a.SubClass]
|
account: a,
|
||||||
)
|
balance: balance,
|
||||||
)
|
balanceCHF: fxService.ConvertCurrency(balance, a.Currency, "CHF", settings.FixerIoApiKey),
|
||||||
|
startBalanceCHF: fxService.ConvertCurrency(a.StartBalance, a.Currency, "CHF", settings.FixerIoApiKey),
|
||||||
|
className: subclassRepository.GetClass(a.Class),
|
||||||
|
subClassName: subClassNameByNumber[a.SubClass]);
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return accountList.OrderBy(a => a.Number).ToList();
|
return accountList.OrderBy(a => a.Number).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static decimal ComputeBalance(
|
||||||
|
Account account,
|
||||||
|
Dictionary<string, List<TransactionDataset>> creditsByAccount,
|
||||||
|
Dictionary<string, List<TransactionDataset>> debitsByAccount)
|
||||||
|
{
|
||||||
|
var balance = account.StartBalance;
|
||||||
|
if (creditsByAccount.TryGetValue(account.Id, out var credits))
|
||||||
|
{
|
||||||
|
balance += credits.Sum(t => t.GetValue(account.IsFxAccount));
|
||||||
|
}
|
||||||
|
if (debitsByAccount.TryGetValue(account.Id, out var debits))
|
||||||
|
{
|
||||||
|
balance -= debits.Sum(t => t.GetValue(account.IsFxAccount));
|
||||||
|
}
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
public BalanceDataset GetBalanceView()
|
public BalanceDataset GetBalanceView()
|
||||||
{
|
{
|
||||||
var accountList = GetAccountViewList();
|
var accountList = GetAccountViewList();
|
||||||
|
|
@ -107,40 +128,20 @@ namespace Schaad.Accounting.Services
|
||||||
return balanceView;
|
return balanceView;
|
||||||
}
|
}
|
||||||
|
|
||||||
private decimal GetBalanceInAccountCurrency(Account a, List<TransactionDataset> transactionList)
|
|
||||||
{
|
|
||||||
var balance = a.StartBalance
|
|
||||||
+ transactionList.Where(t => t.TargetAccountId == a.Id).Sum(t => t.GetValue(a.IsFxAccount))
|
|
||||||
- transactionList.Where(t => t.OriginAccountId == a.Id).Sum(t => t.GetValue(a.IsFxAccount));
|
|
||||||
return balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private decimal GetCurrentBalanceInCHF(Account a, List<TransactionDataset> transactionList)
|
|
||||||
{
|
|
||||||
var settings = settingsService.GetSettings();
|
|
||||||
var balanceInAccountCurrency = GetBalanceInAccountCurrency(a, transactionList);
|
|
||||||
var balanceInChf = fxService.ConvertCurrency(balanceInAccountCurrency, a.Currency, "CHF", settings.FixerIoApiKey);
|
|
||||||
return balanceInChf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get transaction list with the origin and target account for each transaction
|
/// Get transaction list with the origin and target account for each transaction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<TransactionDataset> GetTransactionViewList()
|
public List<TransactionDataset> GetTransactionViewList()
|
||||||
{
|
{
|
||||||
var accountList = accountRepository.GetAccountList();
|
var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id);
|
||||||
var transactionList = transactionRepository.GetTransactionList();
|
var transactionList = transactionRepository.GetTransactionList();
|
||||||
|
|
||||||
var transactionViewList = transactionList.Select(
|
return transactionList.Select(t =>
|
||||||
t =>
|
new TransactionDataset(
|
||||||
new TransactionDataset(
|
t,
|
||||||
t,
|
accountsById[t.OriginAccountId],
|
||||||
accountList.Single(a => a.Id == t.OriginAccountId),
|
accountsById[t.TargetAccountId]))
|
||||||
accountList.Single(a => a.Id == t.TargetAccountId))
|
|
||||||
)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return transactionViewList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -148,53 +149,41 @@ namespace Schaad.Accounting.Services
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<TransactionDataset> GetTransactionViewList(string accountId)
|
public List<TransactionDataset> GetTransactionViewList(string accountId)
|
||||||
{
|
{
|
||||||
var accountList = accountRepository.GetAccountList();
|
var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id);
|
||||||
var transactionList = transactionRepository.GetTransactionList().Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId);
|
var transactionList = transactionRepository.GetTransactionList()
|
||||||
|
.Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId);
|
||||||
|
|
||||||
var transactionViewList = transactionList.Select(
|
return transactionList.Select(t =>
|
||||||
t =>
|
new TransactionDataset(
|
||||||
new TransactionDataset(
|
WithDisplaySign(t),
|
||||||
WithDisplaySign(t),
|
accountsById[t.OriginAccountId],
|
||||||
accountList.Single(a => a.Id == t.OriginAccountId),
|
accountsById[t.TargetAccountId]))
|
||||||
accountList.Single(a => a.Id == t.TargetAccountId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return transactionViewList;
|
|
||||||
|
|
||||||
Transaction WithDisplaySign(Transaction t)
|
Transaction WithDisplaySign(Transaction t)
|
||||||
{
|
{
|
||||||
var account = accountList.Single(a => a.Id == t.OriginAccountId);
|
var originAccount = accountsById[t.OriginAccountId];
|
||||||
|
if (originAccount.Class == ClassIds.Activa && accountId == t.OriginAccountId)
|
||||||
if (account.Class == ClassIds.Activa && accountId == t.OriginAccountId)
|
|
||||||
{
|
{
|
||||||
var copy = new Transaction();
|
var copy = new Transaction();
|
||||||
t.Copy(copy);
|
t.Copy(copy);
|
||||||
copy.Value *= -1;
|
copy.Value *= -1;
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get booking rules with their account
|
/// Get booking rules with their account
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<BookingRuleDataset> GetBookingRuleViewList()
|
public List<BookingRuleDataset> GetBookingRuleViewList()
|
||||||
{
|
{
|
||||||
var accountList = accountRepository.GetAccountList();
|
var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id);
|
||||||
var bookinRuleList = bookingRuleRepository.GetBookingRuleList();
|
var bookingRules = bookingRuleRepository.GetBookingRuleList();
|
||||||
|
|
||||||
return bookinRuleList.Select(
|
return bookingRules.Select(t =>
|
||||||
t =>
|
new BookingRuleDataset(t, accountsById[t.AccountId].Name))
|
||||||
new BookingRuleDataset(
|
|
||||||
t,
|
|
||||||
accountList.Single(a => a.Id == t.AccountId).Name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue