Every repository, service, Blazor page/dialog, and test now uses
async/await. Single atomic diff; the codebase does not compile in
intermediate states.
- BaseRepository: LoadListAsync/LoadAsync/SaveAsync return Task<T>;
per-file locks use SemaphoreSlim so waiters can await; Save
serialises to a MemoryStream sync (XmlSerializer has no async
form), then File.WriteAllBytesAsync + sync File.Move.
- RepositoryCache.GetOrLoadAsync takes a Func<Task<List<T>>>.
- All 7 repository interfaces + implementations async.
- All service interfaces + implementations async (except vendor
IFxService and stateless IFxConverter / SettingsService).
- Every Blazor OnInitializedAsync switches to await base.
- Test suite fully async, 42 tests pass.
AccountRepository.EnsureAccountsFile keeps two .GetAwaiter().GetResult()
bridges because it runs from the constructor.
Null-render guard follow-up (folded in):
Blazor now renders the component once with fields at their initial
values while OnInitializedAsync awaits — so fields declared `= null!`
are actually null on that first render and things like
`accounts.GroupBy(...)` throw ArgumentNullException. Fixed across
Transactions, BalanceReport, BalanceSheetReport, ProfitLossReport,
DetailReport, Assets, Spendings, SpendingsOverTime, TransactionDialog,
TransactionSplitDialog, and BookingRuleDialog:
- Collection fields initialise to [] so first-render loops are empty.
- Single-object data fields become nullable; the razor wraps
consumption in `@if (field is null) { <p>Lädt…</p> return; }`.
- <PlotlyChart> guarded behind a null check on config/layout/data so
Plotly.Blazor's @bind doesn't see nulls.
- Header/footer strings initialise to "" instead of null!.
Architectural hygiene on a single-user local Blazor Server app: the
observed win is one File.ReadAllBytesAsync and one
File.WriteAllBytesAsync per Load/Save, and after PR D each file is
loaded at most once per SignalR circuit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the settings-mutating year-hopping in GetAccountExpensesPerMonth
that discarded the user's header year selection and silently forced
the Spendings-over-time chart back to DateTime.Now.Year. The chart
now honours settingsService.GetYear(), skips accounts with no
transactions instead of returning null, and no longer depends on
IAccountRepository or ISubclassRepository.
Also reorder the plan: async becomes PR F, tests PR G.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>