From 1c7ba7f41679b0ff4dbd8b62db5815af0963a6f4 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Thu, 2 Jul 2026 20:52:24 +0200 Subject: [PATCH] PR D: per-scope repository cache Add a Scoped RepositoryCache that memoizes deserialized XML lists, keyed by absolute file path. Each XML file is loaded at most once per SignalR circuit; Save invalidates the corresponding entry so subsequent reads reload from disk. BaseRepository gains a LoadList(fileName) helper; each of the seven repositories now defines Get*List() as a one-liner over that helper and takes RepositoryCache via the constructor. Co-Authored-By: Claude Opus 4.7 --- .../Repositories/AccountRepository.cs | 10 ++--- .../Repositories/BankTransactionRepository.cs | 9 ++--- .../Repositories/BaseRepository.cs | 23 +++++++++-- .../Repositories/BookingRuleRepository.cs | 9 ++--- .../Repositories/BookingTextRepository.cs | 11 ++---- .../SplitPredefinitionRepository.cs | 9 ++--- .../Repositories/SubclassRepository.cs | 11 ++---- .../Repositories/TransactionRepository.cs | 9 ++--- Schaad.Accounting.Db/RepositoryCache.cs | 38 +++++++++++++++++++ Schaad.Accounting.UI/Extensions.cs | 1 + 10 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 Schaad.Accounting.Db/RepositoryCache.cs diff --git a/Schaad.Accounting.Db/Repositories/AccountRepository.cs b/Schaad.Accounting.Db/Repositories/AccountRepository.cs index 1061dc9..11a7070 100644 --- a/Schaad.Accounting.Db/Repositories/AccountRepository.cs +++ b/Schaad.Accounting.Db/Repositories/AccountRepository.cs @@ -11,20 +11,16 @@ namespace Schaad.Accounting.Repositories { private readonly string ACCOUNTS = "Accounts.xml"; - public AccountRepository(ISettingsService settingsService) : base(settingsService) + public AccountRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { EnsureAccountsFile(); } - /// /// Load accounts /// - public List GetAccountList() - { - var accounts = Load>(ACCOUNTS); - return accounts ?? new List(); - } + public List GetAccountList() => LoadList(ACCOUNTS); /// /// Save an account (insert/update) diff --git a/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs b/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs index 10d3c6a..9b3c970 100644 --- a/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs @@ -10,18 +10,15 @@ namespace Schaad.Accounting.Repositories { private readonly string BANK_TRANSACTIONS = "BankTransactions.xml"; - public BankTransactionRepository(ISettingsService settingsService) : base(settingsService) + public BankTransactionRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { } /// /// Load bank transactions /// - public List GetBankTransactionList() - { - var transactions = Load>(BANK_TRANSACTIONS); - return transactions != null ? transactions : new List(); - } + public List GetBankTransactionList() => LoadList(BANK_TRANSACTIONS); /// /// Save new bank transactions, ignore existing ones (no update) diff --git a/Schaad.Accounting.Db/Repositories/BaseRepository.cs b/Schaad.Accounting.Db/Repositories/BaseRepository.cs index 4411e15..3813fa8 100644 --- a/Schaad.Accounting.Db/Repositories/BaseRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BaseRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; @@ -16,10 +17,12 @@ namespace Schaad.Accounting.Repositories new(StringComparer.OrdinalIgnoreCase); protected readonly ISettingsService settingsService; + protected readonly RepositoryCache cache; - protected BaseRepository(ISettingsService settingsService) + protected BaseRepository(ISettingsService settingsService, RepositoryCache cache) { this.settingsService = settingsService; + this.cache = cache; } protected void EnsureFileExists(string fileName) @@ -38,9 +41,20 @@ namespace Schaad.Accounting.Repositories } } + /// + /// Load a list, cached per scope. First call reads the XML file; subsequent calls in + /// the same scope return the cached list. invalidates the entry. + /// + protected List LoadList(string fileName) + { + var filePath = Path.Combine(settingsService.GetDbPath(), fileName); + return cache.GetOrLoad(filePath, () => Load>(fileName) ?? new List()); + } + /// /// Save an object to an xml file. Writes to a .tmp sibling and then atomically - /// renames it, so a crash mid-write leaves the previous file intact. + /// renames it, so a crash mid-write leaves the previous file intact. Invalidates + /// the corresponding cache entry. /// protected void Save(T obj, string fileName) { @@ -74,10 +88,13 @@ namespace Schaad.Accounting.Repositories throw; } } + + cache.Invalidate(filePath); } /// - /// Load an object from an xml file + /// Load an object from an xml file (uncached; used for one-off reads such as file + /// existence bootstrapping). Prefer for repository reads. /// protected T Load(string fileName) { diff --git a/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs b/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs index 619fce4..34c04b1 100644 --- a/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs @@ -10,7 +10,8 @@ namespace Schaad.Accounting.Repositories { private readonly string BOOKING_RULES = "BookingRules.xml"; - public BookingRuleRepository(ISettingsService settingsService) : base(settingsService) + public BookingRuleRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { EnsureFileExists(BOOKING_RULES); } @@ -18,11 +19,7 @@ namespace Schaad.Accounting.Repositories /// /// Load booking rules /// - public List GetBookingRuleList() - { - var bookingTexts = Load>(BOOKING_RULES); - return bookingTexts ?? new List(); - } + public List GetBookingRuleList() => LoadList(BOOKING_RULES); /// /// Save a booking rule (insert/update) diff --git a/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs b/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs index d3fa0c7..06c66ec 100644 --- a/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs @@ -10,19 +10,16 @@ namespace Schaad.Accounting.Repositories { private readonly string BOOKING_TEXTS = "BookingTexts.xml"; - public BookingTextRepository(ISettingsService settingsService) : base(settingsService) + public BookingTextRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { EnsureFileExists(BOOKING_TEXTS); } /// - /// Load bokking texts + /// Load booking texts /// - public List GetBookingTextList() - { - var bookingTexts = Load>(BOOKING_TEXTS); - return bookingTexts ?? new List(); - } + public List GetBookingTextList() => LoadList(BOOKING_TEXTS); /// /// Save a booking text (insert/update) diff --git a/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs b/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs index ac80d29..a73dae8 100644 --- a/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs @@ -10,7 +10,8 @@ namespace Schaad.Accounting.Repositories { private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml"; - public SplitPredefinitionRepository(ISettingsService settingsService) : base(settingsService) + public SplitPredefinitionRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { EnsureFileExists(SPLIT_PREDEFINITION); } @@ -18,11 +19,7 @@ namespace Schaad.Accounting.Repositories /// /// Load split predefinitions /// - public List GetSplitPredefinitionList() - { - var definitions = Load>(SPLIT_PREDEFINITION); - return definitions ?? new List(); - } + public List GetSplitPredefinitionList() => LoadList(SPLIT_PREDEFINITION); /// /// Save a split predefinition (insert/update) diff --git a/Schaad.Accounting.Db/Repositories/SubclassRepository.cs b/Schaad.Accounting.Db/Repositories/SubclassRepository.cs index f2ecb7c..1f4f54f 100644 --- a/Schaad.Accounting.Db/Repositories/SubclassRepository.cs +++ b/Schaad.Accounting.Db/Repositories/SubclassRepository.cs @@ -11,7 +11,8 @@ namespace Schaad.Accounting.Repositories private readonly Dictionary classes = new Dictionary(); private readonly string SUBCLASSES = "SubClasses.xml"; - public SubclassRepository(ISettingsService settingsService) : base(settingsService) + public SubclassRepository(ISettingsService settingsService, RepositoryCache cache) + : base(settingsService, cache) { EnsureFileExists(SUBCLASSES); @@ -22,13 +23,9 @@ namespace Schaad.Accounting.Repositories } /// - /// Load bokking texts + /// Load subclasses /// - public List GetSubClassList() - { - var subclasses = Load>(SUBCLASSES); - return subclasses ?? new List(); - } + public List GetSubClassList() => LoadList(SUBCLASSES); /// /// Save a booking text (insert/update) diff --git a/Schaad.Accounting.Db/Repositories/TransactionRepository.cs b/Schaad.Accounting.Db/Repositories/TransactionRepository.cs index ddf294a..0e55408 100644 --- a/Schaad.Accounting.Db/Repositories/TransactionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/TransactionRepository.cs @@ -11,7 +11,8 @@ namespace Schaad.Accounting.Repositories private readonly IAccountRepository accountRepository; private readonly string TRANSACTIONS = "Transactions.xml"; - public TransactionRepository(ISettingsService settingsService, IAccountRepository accountRepository) : base(settingsService) + public TransactionRepository(ISettingsService settingsService, RepositoryCache cache, IAccountRepository accountRepository) + : base(settingsService, cache) { this.accountRepository = accountRepository; } @@ -19,11 +20,7 @@ namespace Schaad.Accounting.Repositories /// /// Get transaction list /// - public List GetTransactionList() - { - var transactionList = Load>(TRANSACTIONS); - return transactionList ?? new List(); - } + public List GetTransactionList() => LoadList(TRANSACTIONS); /// /// Save a transaction (insert/update) diff --git a/Schaad.Accounting.Db/RepositoryCache.cs b/Schaad.Accounting.Db/RepositoryCache.cs new file mode 100644 index 0000000..e1834af --- /dev/null +++ b/Schaad.Accounting.Db/RepositoryCache.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Schaad.Accounting.Repositories +{ + /// + /// Per-scope cache of deserialized XML lists, keyed by absolute file path. + /// + /// Registered as Scoped so it lives for the SignalR circuit; each XML file is + /// then loaded at most once per circuit. Writes go through , + /// which invalidates the corresponding entry so the next read reloads from disk. + /// + /// Not thread-safe — callers within a single Blazor Server circuit run serialized on the + /// circuit's synchronization context, so no locking is needed. + /// + public class RepositoryCache + { + private readonly Dictionary entries = + new(StringComparer.OrdinalIgnoreCase); + + public List GetOrLoad(string cacheKey, Func> loader) + { + if (entries.TryGetValue(cacheKey, out var cached)) + { + return (List)cached; + } + + var list = loader(); + entries[cacheKey] = list; + return list; + } + + public void Invalidate(string cacheKey) + { + entries.Remove(cacheKey); + } + } +} diff --git a/Schaad.Accounting.UI/Extensions.cs b/Schaad.Accounting.UI/Extensions.cs index b564a45..384173b 100644 --- a/Schaad.Accounting.UI/Extensions.cs +++ b/Schaad.Accounting.UI/Extensions.cs @@ -16,6 +16,7 @@ namespace Schaad.Accounting.UI // can safely become Scoped (see IMPROVEMENT_PLAN.md). services.AddSingleton(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();