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<T>(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 <noreply@anthropic.com>
This commit is contained in:
parent
8961bb234c
commit
1c7ba7f416
10 changed files with 82 additions and 48 deletions
|
|
@ -11,20 +11,16 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
private readonly string ACCOUNTS = "Accounts.xml";
|
private readonly string ACCOUNTS = "Accounts.xml";
|
||||||
|
|
||||||
public AccountRepository(ISettingsService settingsService) : base(settingsService)
|
public AccountRepository(ISettingsService settingsService, RepositoryCache cache)
|
||||||
|
: base(settingsService, cache)
|
||||||
{
|
{
|
||||||
EnsureAccountsFile();
|
EnsureAccountsFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load accounts
|
/// Load accounts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Account> GetAccountList()
|
public List<Account> GetAccountList() => LoadList<Account>(ACCOUNTS);
|
||||||
{
|
|
||||||
var accounts = Load<List<Account>>(ACCOUNTS);
|
|
||||||
return accounts ?? new List<Account>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save an account (insert/update)
|
/// Save an account (insert/update)
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,15 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
private readonly string BANK_TRANSACTIONS = "BankTransactions.xml";
|
private readonly string BANK_TRANSACTIONS = "BankTransactions.xml";
|
||||||
|
|
||||||
public BankTransactionRepository(ISettingsService settingsService) : base(settingsService)
|
public BankTransactionRepository(ISettingsService settingsService, RepositoryCache cache)
|
||||||
|
: base(settingsService, cache)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load bank transactions
|
/// Load bank transactions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<BankTransaction> GetBankTransactionList()
|
public List<BankTransaction> GetBankTransactionList() => LoadList<BankTransaction>(BANK_TRANSACTIONS);
|
||||||
{
|
|
||||||
var transactions = Load<List<BankTransaction>>(BANK_TRANSACTIONS);
|
|
||||||
return transactions != null ? transactions : new List<BankTransaction>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save new bank transactions, ignore existing ones (no update)
|
/// Save new bank transactions, ignore existing ones (no update)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
|
@ -16,10 +17,12 @@ namespace Schaad.Accounting.Repositories
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
protected readonly ISettingsService settingsService;
|
protected readonly ISettingsService settingsService;
|
||||||
|
protected readonly RepositoryCache cache;
|
||||||
|
|
||||||
protected BaseRepository(ISettingsService settingsService)
|
protected BaseRepository(ISettingsService settingsService, RepositoryCache cache)
|
||||||
{
|
{
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
|
this.cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void EnsureFileExists(string fileName)
|
protected void EnsureFileExists(string fileName)
|
||||||
|
|
@ -38,9 +41,20 @@ namespace Schaad.Accounting.Repositories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load a list, cached per scope. First call reads the XML file; subsequent calls in
|
||||||
|
/// the same scope return the cached list. <see cref="Save"/> invalidates the entry.
|
||||||
|
/// </summary>
|
||||||
|
protected List<T> LoadList<T>(string fileName)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(settingsService.GetDbPath(), fileName);
|
||||||
|
return cache.GetOrLoad(filePath, () => Load<List<T>>(fileName) ?? new List<T>());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save an object to an xml file. Writes to a .tmp sibling and then atomically
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected void Save<T>(T obj, string fileName)
|
protected void Save<T>(T obj, string fileName)
|
||||||
{
|
{
|
||||||
|
|
@ -74,10 +88,13 @@ namespace Schaad.Accounting.Repositories
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.Invalidate(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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 <see cref="LoadList{T}"/> for repository reads.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected T Load<T>(string fileName)
|
protected T Load<T>(string fileName)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
private readonly string BOOKING_RULES = "BookingRules.xml";
|
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);
|
EnsureFileExists(BOOKING_RULES);
|
||||||
}
|
}
|
||||||
|
|
@ -18,11 +19,7 @@ namespace Schaad.Accounting.Repositories
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load booking rules
|
/// Load booking rules
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<BookingRule> GetBookingRuleList()
|
public List<BookingRule> GetBookingRuleList() => LoadList<BookingRule>(BOOKING_RULES);
|
||||||
{
|
|
||||||
var bookingTexts = Load<List<BookingRule>>(BOOKING_RULES);
|
|
||||||
return bookingTexts ?? new List<BookingRule>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a booking rule (insert/update)
|
/// Save a booking rule (insert/update)
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,16 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
private readonly string BOOKING_TEXTS = "BookingTexts.xml";
|
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);
|
EnsureFileExists(BOOKING_TEXTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load bokking texts
|
/// Load booking texts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<BookingText> GetBookingTextList()
|
public List<BookingText> GetBookingTextList() => LoadList<BookingText>(BOOKING_TEXTS);
|
||||||
{
|
|
||||||
var bookingTexts = Load<List<BookingText>>(BOOKING_TEXTS);
|
|
||||||
return bookingTexts ?? new List<BookingText>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a booking text (insert/update)
|
/// Save a booking text (insert/update)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
{
|
{
|
||||||
private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml";
|
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);
|
EnsureFileExists(SPLIT_PREDEFINITION);
|
||||||
}
|
}
|
||||||
|
|
@ -18,11 +19,7 @@ namespace Schaad.Accounting.Repositories
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load split predefinitions
|
/// Load split predefinitions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SplitPredefinition> GetSplitPredefinitionList()
|
public List<SplitPredefinition> GetSplitPredefinitionList() => LoadList<SplitPredefinition>(SPLIT_PREDEFINITION);
|
||||||
{
|
|
||||||
var definitions = Load<List<SplitPredefinition>>(SPLIT_PREDEFINITION);
|
|
||||||
return definitions ?? new List<SplitPredefinition>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a split predefinition (insert/update)
|
/// Save a split predefinition (insert/update)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
private readonly Dictionary<int, string> classes = new Dictionary<int, string>();
|
private readonly Dictionary<int, string> classes = new Dictionary<int, string>();
|
||||||
private readonly string SUBCLASSES = "SubClasses.xml";
|
private readonly string SUBCLASSES = "SubClasses.xml";
|
||||||
|
|
||||||
public SubclassRepository(ISettingsService settingsService) : base(settingsService)
|
public SubclassRepository(ISettingsService settingsService, RepositoryCache cache)
|
||||||
|
: base(settingsService, cache)
|
||||||
{
|
{
|
||||||
EnsureFileExists(SUBCLASSES);
|
EnsureFileExists(SUBCLASSES);
|
||||||
|
|
||||||
|
|
@ -22,13 +23,9 @@ namespace Schaad.Accounting.Repositories
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load bokking texts
|
/// Load subclasses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SubClass> GetSubClassList()
|
public List<SubClass> GetSubClassList() => LoadList<SubClass>(SUBCLASSES);
|
||||||
{
|
|
||||||
var subclasses = Load<List<SubClass>>(SUBCLASSES);
|
|
||||||
return subclasses ?? new List<SubClass>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a booking text (insert/update)
|
/// Save a booking text (insert/update)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ namespace Schaad.Accounting.Repositories
|
||||||
private readonly IAccountRepository accountRepository;
|
private readonly IAccountRepository accountRepository;
|
||||||
private readonly string TRANSACTIONS = "Transactions.xml";
|
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;
|
this.accountRepository = accountRepository;
|
||||||
}
|
}
|
||||||
|
|
@ -19,11 +20,7 @@ namespace Schaad.Accounting.Repositories
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get transaction list
|
/// Get transaction list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Transaction> GetTransactionList()
|
public List<Transaction> GetTransactionList() => LoadList<Transaction>(TRANSACTIONS);
|
||||||
{
|
|
||||||
var transactionList = Load<List<Transaction>>(TRANSACTIONS);
|
|
||||||
return transactionList ?? new List<Transaction>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a transaction (insert/update)
|
/// Save a transaction (insert/update)
|
||||||
|
|
|
||||||
38
Schaad.Accounting.Db/RepositoryCache.cs
Normal file
38
Schaad.Accounting.Db/RepositoryCache.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.Repositories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="BaseRepository.Save"/>,
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class RepositoryCache
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> entries =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public List<T> GetOrLoad<T>(string cacheKey, Func<List<T>> loader)
|
||||||
|
{
|
||||||
|
if (entries.TryGetValue(cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
return (List<T>)cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = loader();
|
||||||
|
entries[cacheKey] = list;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate(string cacheKey)
|
||||||
|
{
|
||||||
|
entries.Remove(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ namespace Schaad.Accounting.UI
|
||||||
// can safely become Scoped (see IMPROVEMENT_PLAN.md).
|
// can safely become Scoped (see IMPROVEMENT_PLAN.md).
|
||||||
services.AddSingleton<ISettingsService, SettingsService>();
|
services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
|
|
||||||
|
services.AddScoped<RepositoryCache>();
|
||||||
services.AddScoped<IAccountRepository, AccountRepository>();
|
services.AddScoped<IAccountRepository, AccountRepository>();
|
||||||
services.AddScoped<IBankTransactionRepository, BankTransactionRepository>();
|
services.AddScoped<IBankTransactionRepository, BankTransactionRepository>();
|
||||||
services.AddScoped<IBookingRuleRepository, BookingRuleRepository>();
|
services.AddScoped<IBookingRuleRepository, BookingRuleRepository>();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue