From db99caca1a2f0941a347f8510aee7d0b18517935 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Sun, 19 Apr 2026 16:35:17 +0100 Subject: [PATCH 1/2] Refactor cache expiration to use timeout seconds param Refactored ApplicationCache and related view models to accept an integer timeout (in seconds) for cache expiration instead of requiring MemoryCacheEntryOptions. The ApplicationCache class now constructs MemoryCacheEntryOptions internally based on the provided timeout. Updated all usages and tests to pass timeout values, simplifying cache management and reducing repetitive code. SetAccessToken remains flexible, still accepting MemoryCacheEntryOptions. --- .../ViewModelTests/LoginPageViewModelTests.cs | 2 +- .../MyAccount/MyAccountPageViewModelTests.cs | 2 +- .../Services/ApplicationCache.cs | 89 +++++++++++-------- .../ViewModels/LoginPageViewModel.cs | 12 +-- .../MyAccountAddressPageViewModel.cs | 14 +-- .../MyAccountContactPageViewModel.cs | 12 +-- .../MyAccount/MyAccountPageViewModel.cs | 12 +-- 7 files changed, 59 insertions(+), 84 deletions(-) diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs index 49da0875..f6192029 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs @@ -95,7 +95,7 @@ public void LoginPageViewModel_LoginCommand_Execute_ConfigUrlSet_IsExecuted(Stri this.Mediator.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); this.NavigationService.Verify(n => n.GoToHome(), Times.Once); if (String.IsNullOrEmpty(configUrl) == false){ - this.ApplicationCache.Verify(v => v.SetConfigHostUrl(It.IsAny(), It.IsAny()), Times.Once); + this.ApplicationCache.Verify(v => v.SetConfigHostUrl(It.IsAny(), It.IsAny()), Times.Once); } } diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs index ccc7ba53..0ef73a7d 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs @@ -56,7 +56,7 @@ public async Task MyAccountPageViewModel_Initialise_IsInitialised() { this.ViewModel.MerchantName.ShouldBe(TestData.MerchantDetailsModel.MerchantName); this.ViewModel.LastLogin.ShouldBe(DateTime.Now, TimeSpan.FromSeconds(30)); this.ViewModel.IsDarkThemeEnabled.ShouldBeTrue(); - this.ApplicationCache.Verify(a => a.SetMerchantDetails(It.IsAny(), It.IsAny()), Times.Once); + this.ApplicationCache.Verify(a => a.SetMerchantDetails(It.IsAny(), It.IsAny()), Times.Once); } [Fact] diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs index 1640bdcf..ab5de460 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs @@ -1,10 +1,11 @@ -using System; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; using TransactionProcessor.Mobile.BusinessLogic.Models; namespace TransactionProcessor.Mobile.BusinessLogic.Services @@ -13,43 +14,41 @@ public interface IApplicationCache { String GetConfigHostUrl(); - void SetConfigHostUrl(String value, MemoryCacheEntryOptions options = default); + void SetConfigHostUrl(String value, Int32 timeout=60); Boolean GetUseTrainingMode(); - void SetUseTrainingMode(Boolean value, MemoryCacheEntryOptions options = default); + void SetUseTrainingMode(Boolean value, Int32 timeout = 60); Boolean GetIsLoggedIn(); - void SetIsLoggedIn(Boolean value, MemoryCacheEntryOptions options = default); + void SetIsLoggedIn(Boolean value, Int32 timeout = 60); Configuration GetConfiguration(); - void SetConfiguration(Configuration value, MemoryCacheEntryOptions options = default); + void SetConfiguration(Configuration value, Int32 timeout = 60); List GetContractProducts(); - void SetContractProducts(List value, MemoryCacheEntryOptions options = default); + void SetContractProducts(List value, Int32 timeout = 60); TokenResponseModel GetAccessToken(); - void SetAccessToken(TokenResponseModel value, MemoryCacheEntryOptions options = default); - + void SetAccessToken(TokenResponseModel value, MemoryCacheEntryOptions cacheEntryOptions = null); Guid GetEstateId(); - void SetEstateId(Guid value, MemoryCacheEntryOptions options = default); + void SetEstateId(Guid value, Int32 timeout = 60); Guid GetMerchantId(); - void SetMerchantId(Guid value, MemoryCacheEntryOptions options = default); - + void SetMerchantId(Guid value, Int32 timeout = 60); MerchantDetailsModel GetMerchantDetails(); - void SetMerchantDetails(MerchantDetailsModel value, MemoryCacheEntryOptions options = default); + void SetMerchantDetails(MerchantDetailsModel value, Int32 timeout = 60); Decimal GetMerchantBalance(); - void SetMerchantBalance(Decimal value, MemoryCacheEntryOptions options = default); + void SetMerchantBalance(Decimal value); } [ExcludeFromCodeCoverage] @@ -112,9 +111,9 @@ public String GetConfigHostUrl() } public void SetConfigHostUrl(String value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("ConfigHostUrl", value, options); + this.Set("ConfigHostUrl", value, timeout); } public Boolean GetUseTrainingMode() @@ -123,39 +122,39 @@ public Boolean GetUseTrainingMode() } public void SetAccessToken(TokenResponseModel value, - MemoryCacheEntryOptions options = default) + MemoryCacheEntryOptions cacheEntryOptions = null) { - this.Set("AccessToken", value, options); + this.Set("AccessToken", value, cacheEntryOptions); } public void SetConfiguration(Configuration value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("Configuration", value, options); + this.Set("Configuration", value, timeout); } public void SetContractProducts(List value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("ContractProducts", value, options); + this.Set("ContractProducts", value, timeout); } public void SetEstateId(Guid value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("EstateId", value, options); + this.Set("EstateId", value, timeout); } public void SetIsLoggedIn(Boolean value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("isLoggedIn", value, options); + this.Set("isLoggedIn", value, timeout); } public void SetMerchantId(Guid value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("MerchantId", value, options); + this.Set("MerchantId", value, timeout); } public MerchantDetailsModel GetMerchantDetails() @@ -164,31 +163,47 @@ public MerchantDetailsModel GetMerchantDetails() } public void SetMerchantDetails(MerchantDetailsModel value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) { - this.Set("MerchantDetails", value, options); + this.Set("MerchantDetails", value, timeout); } public Decimal GetMerchantBalance() { return this.TryGetValue("MerchantBalance"); } - public void SetMerchantBalance(Decimal value, - MemoryCacheEntryOptions options = default) { - this.Set("MerchantBalance", value, options); + public void SetMerchantBalance(Decimal value) { + this.Set("MerchantBalance", value, 60); } public void SetUseTrainingMode(Boolean value, - MemoryCacheEntryOptions options = default) + Int32 timeout = 60) + { + this.Set("UseTrainingMode", value, timeout); + } + + private void Set(String key, + T cache, + Int32 timeout) { - this.Set("UseTrainingMode", value, options); + DateTime expirationTime = DateTime.Now.AddSeconds(timeout); + CancellationChangeToken expirationToken = new(new CancellationTokenSource(TimeSpan.FromSeconds(timeout)).Token); + MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() + // Pin to cache. + .SetPriority(CacheItemPriority.NeverRemove) + // Set the actual expiration time + .SetAbsoluteExpiration(expirationTime) + // Force eviction to run + .AddExpirationToken(expirationToken); + + this.MemoryCache.Set(key, cache, cacheEntryOptions); } private void Set(String key, T cache, - MemoryCacheEntryOptions options) + MemoryCacheEntryOptions cacheEntryOptions) { - this.MemoryCache.Set(key, cache, options); + this.MemoryCache.Set(key, cache, cacheEntryOptions); } private T TryGetValue(String Key) diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs index 0a422647..01d584db 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs @@ -146,17 +146,7 @@ private async Task>> GetMerchantContractProduc private void CacheContractData(List contractProductModels) { - DateTime expirationTime = DateTime.Now.AddMinutes(60); - CancellationChangeToken expirationToken = new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(60)).Token); - MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() - // Pin to cache. - .SetPriority(CacheItemPriority.NeverRemove) - // Set the actual expiration time - .SetAbsoluteExpiration(expirationTime) - // Force eviction to run - .AddExpirationToken(expirationToken); - - this.ApplicationCache.SetContractProducts(contractProductModels, cacheEntryOptions); + this.ApplicationCache.SetContractProducts(contractProductModels, 600); } private async Task CheckForUpdates(Configuration configuration) { diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountAddressPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountAddressPageViewModel.cs index d68bb689..22b73427 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountAddressPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountAddressPageViewModel.cs @@ -54,18 +54,8 @@ public async Task Initialise(CancellationToken cancellationToken) { await this.DialogService.ShowWarningToast("Unable to load merchant details. Please try again later.", cancellationToken: cancellationToken); return; } - - DateTime expirationTime = DateTime.Now.AddMinutes(60); - CancellationChangeToken expirationToken = new(new CancellationTokenSource(TimeSpan.FromMinutes(60)).Token); - MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() - // Pin to cache. - .SetPriority(CacheItemPriority.NeverRemove) - // Set the actual expiration time - .SetAbsoluteExpiration(expirationTime) - // Force eviction to run - .AddExpirationToken(expirationToken); - - this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, cacheEntryOptions); + + this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, 3600); merchantDetails = this.ApplicationCache.GetMerchantDetails(); } diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountContactPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountContactPageViewModel.cs index f0f46532..1387b6b5 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountContactPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountContactPageViewModel.cs @@ -48,17 +48,7 @@ public async Task Initialise(CancellationToken cancellationToken) { return; } - DateTime expirationTime = DateTime.Now.AddMinutes(60); - CancellationChangeToken expirationToken = new(new CancellationTokenSource(TimeSpan.FromMinutes(60)).Token); - MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() - // Pin to cache. - .SetPriority(CacheItemPriority.NeverRemove) - // Set the actual expiration time - .SetAbsoluteExpiration(expirationTime) - // Force eviction to run - .AddExpirationToken(expirationToken); - - this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, cacheEntryOptions); + this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, 3600); merchantDetails = this.ApplicationCache.GetMerchantDetails(); } diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs index 894021b3..664effc0 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs @@ -85,17 +85,7 @@ public async Task Initialise(CancellationToken cancellationToken) { this.MerchantName = merchantDetailsResult.Data.MerchantName; - DateTime expirationTime = DateTime.Now.AddMinutes(60); - CancellationChangeToken expirationToken = new(new CancellationTokenSource(TimeSpan.FromMinutes(60)).Token); - MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() - // Pin to cache. - .SetPriority(CacheItemPriority.NeverRemove) - // Set the actual expiration time - .SetAbsoluteExpiration(expirationTime) - // Force eviction to run - .AddExpirationToken(expirationToken); - - this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, cacheEntryOptions); + this.ApplicationCache.SetMerchantDetails(merchantDetailsResult.Data, 3600); this.LastLogin = DateTime.Now; // TODO: might cache this in the application this.IsDarkThemeEnabled = await this.ApplicationThemeService.GetDarkThemeEnabled(); From cd5d19913bcb2f0d656d5f87a02409d4d0119b7c Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Mon, 20 Apr 2026 06:56:47 +0100 Subject: [PATCH 2/2] move balance to background thread --- .../Services/BalanceRefresher.cs | 20 +++++++++++-------- TransactionProcessor.Mobile/MauiProgram.cs | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs index d87e59a1..7ec7d789 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; +using System.Threading.Tasks; using TransactionProcessor.Mobile.BusinessLogic.Logging; namespace TransactionProcessor.Mobile.BusinessLogic.Services @@ -25,7 +27,8 @@ public BalanceRefresher(IApplicationCache applicationCache, IMerchantService mer public void StartRefreshing() { _cts = new CancellationTokenSource(); - _ = RefreshLoopAsync(_cts.Token); + // Run the refresh loop on a background thread so it does not run on the main UI thread. + _ = Task.Run(() => RefreshLoopAsync(_cts.Token)); } public void StopRefreshing() @@ -40,11 +43,11 @@ private async Task RefreshLoopAsync(CancellationToken token) try { // Do the first refresh - await RefreshBalanceAsync(); + await RefreshBalanceAsync(token).ConfigureAwait(false); - while (await timer.WaitForNextTickAsync(token)) + while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) { - await RefreshBalanceAsync(); + await RefreshBalanceAsync(token).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -52,16 +55,17 @@ private async Task RefreshLoopAsync(CancellationToken token) } } - private async Task RefreshBalanceAsync() + private async Task RefreshBalanceAsync(CancellationToken token) { - decimal balance = await GetBalanceFromApi(); + decimal balance = await GetBalanceFromApi(token).ConfigureAwait(false); + // Setting the cache is lightweight; do on background thread to avoid UI thread work. this.ApplicationCache.SetMerchantBalance(balance); } - private async Task GetBalanceFromApi() + private async Task GetBalanceFromApi(CancellationToken token) { - var result = await this.MerchantService.GetMerchantBalance(CancellationToken.None); + var result = await this.MerchantService.GetMerchantBalance(token).ConfigureAwait(false); if (result.IsSuccess) { return result.Data; diff --git a/TransactionProcessor.Mobile/MauiProgram.cs b/TransactionProcessor.Mobile/MauiProgram.cs index 2013cef1..1a86314f 100644 --- a/TransactionProcessor.Mobile/MauiProgram.cs +++ b/TransactionProcessor.Mobile/MauiProgram.cs @@ -8,6 +8,7 @@ using TransactionProcessor.Mobile.Extensions; using TransactionProcessor.Mobile.UIServices; using Microsoft.Extensions.DependencyInjection; +//using Sentry; namespace TransactionProcessor.Mobile { @@ -25,6 +26,7 @@ public static MauiApp CreateMauiApp() .ConfigureAppServices() .ConfigureUIServices() .ConfigureDatabase() + //.ConfigureSentry() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }).Services.AddTransient() @@ -33,7 +35,7 @@ public static MauiApp CreateMauiApp() builder.Logging.SetMinimumLevel(LogLevel.Trace);//.AddConsole(); Container = builder.Build(); - + Logger.Initialise(new ConsoleLogger()); return Container;