From 666b261fa6a733ae4684f7f58696469a61e9b844 Mon Sep 17 00:00:00 2001 From: David Ortinau Date: Mon, 27 Apr 2026 18:29:49 -0500 Subject: [PATCH] Fix fire-and-forget sync: wrap TriggerSyncAsync in logged TriggerSync() helper Replace all 29 _syncService?.TriggerSyncAsync().ConfigureAwait(false) call sites across 9 repository files with a private TriggerSync() helper that runs the sync in Task.Run and catches/logs any exception via ILogger. Previously, unobserved task exceptions were silently discarded in .NET 5+. The .ConfigureAwait(false) on a non-awaited Task was also a no-op. Affected files: - LearningResourceRepository.cs (12 sites) - ScenarioRepository.cs (2 sites) - SkillProfileRepository.cs (2 sites) - StoryRepository.cs (2 sites) - StreamHistoryRepository.cs (2 sites) - UserActivityRepository.cs (2 sites) - UserProfileRepository.cs (3 sites) - VocabularyLearningContextRepository.cs (2 sites) - VocabularyProgressRepository.cs (2 sites) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Data/LearningResourceRepository.cs | 34 ++++++++++++------- .../Data/ScenarioRepository.cs | 14 ++++++-- .../Data/SkillProfileRepository.cs | 14 ++++++-- .../Data/StoryRepository.cs | 14 ++++++-- .../Data/StreamHistoryRepository.cs | 14 ++++++-- .../Data/UserActivityRepository.cs | 14 ++++++-- .../Data/UserProfileRepository.cs | 16 +++++++-- .../VocabularyLearningContextRepository.cs | 14 ++++++-- .../Data/VocabularyProgressRepository.cs | 14 ++++++-- 9 files changed, 119 insertions(+), 29 deletions(-) diff --git a/src/SentenceStudio.Shared/Data/LearningResourceRepository.cs b/src/SentenceStudio.Shared/Data/LearningResourceRepository.cs index 49984d90..18e7d121 100644 --- a/src/SentenceStudio.Shared/Data/LearningResourceRepository.cs +++ b/src/SentenceStudio.Shared/Data/LearningResourceRepository.cs @@ -144,7 +144,7 @@ public async Task SaveWordAsync(VocabularyWord word) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -279,7 +279,7 @@ public async Task SaveResourceAsync(LearningResource resource) await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return resource.Id; } @@ -301,7 +301,7 @@ public async Task DeleteResourceAsync(LearningResource resource) db.LearningResources.Remove(resource); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -409,7 +409,7 @@ public async Task AddVocabularyToResourceAsync(string resourceId, string v resource.Vocabulary.Add(vocabularyWord); await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -443,7 +443,7 @@ public async Task RemoveVocabularyFromResourceAsync(string resourceId, str resource.Vocabulary.Remove(vocabularyToRemove); await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -740,7 +740,7 @@ public async Task DeleteVocabularyWordAsync(string wordId) await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -765,7 +765,7 @@ public async Task UpdateVocabularyWordAsync(VocabularyWord word) db.VocabularyWords.Update(word); await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -796,7 +796,7 @@ public async Task UpdateVocabularyWordTermsAsync(string wordId, string tar await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -837,7 +837,7 @@ public async Task BulkAssociateWordsWithResourceAsync(string resourceId, L await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -865,7 +865,7 @@ public async Task BulkRemoveWordsFromResourceAsync(string resourceId, List db.ResourceVocabularyMappings.RemoveRange(mappingsToRemove); await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -902,7 +902,7 @@ public async Task BulkDeleteVocabularyWordsAsync(List vocabularyWo await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return true; } @@ -1056,9 +1056,19 @@ public async Task MergeVocabularyWordsAsync(string keeperWordId, List 0) { await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); } return deleted; } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/ScenarioRepository.cs b/src/SentenceStudio.Shared/Data/ScenarioRepository.cs index d28757a8..19d40920 100644 --- a/src/SentenceStudio.Shared/Data/ScenarioRepository.cs +++ b/src/SentenceStudio.Shared/Data/ScenarioRepository.cs @@ -136,7 +136,7 @@ public async Task SaveAsync(ConversationScenario scenario) } await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); _logger.LogInformation("Saved scenario: {Name} (ID: {Id})", scenario.Name, scenario.Id); return scenario.Id; @@ -182,7 +182,7 @@ public async Task DeleteAsync(int id) db.ConversationScenarios.Remove(scenario); await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); _logger.LogInformation("Deleted scenario: {Name} (ID: {Id})", scenario.Name, id); return true; @@ -193,4 +193,14 @@ public async Task DeleteAsync(int id) return false; } } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/SkillProfileRepository.cs b/src/SentenceStudio.Shared/Data/SkillProfileRepository.cs index 5c1d0b43..e1a888fb 100755 --- a/src/SentenceStudio.Shared/Data/SkillProfileRepository.cs +++ b/src/SentenceStudio.Shared/Data/SkillProfileRepository.cs @@ -74,7 +74,7 @@ public async Task SaveAsync(SkillProfile item) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return item.Id; } @@ -95,7 +95,7 @@ public async Task DeleteAsync(SkillProfile item) db.SkillProfiles.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -119,4 +119,14 @@ public async Task DeleteAsync(SkillProfile item) var db = scope.ServiceProvider.GetRequiredService(); return await db.SkillProfiles.FirstOrDefaultAsync(s => s.Id == skillId); } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/StoryRepository.cs b/src/SentenceStudio.Shared/Data/StoryRepository.cs index 659ca415..ae71edcf 100755 --- a/src/SentenceStudio.Shared/Data/StoryRepository.cs +++ b/src/SentenceStudio.Shared/Data/StoryRepository.cs @@ -51,7 +51,7 @@ public async Task SaveAsync(Story item) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -76,7 +76,7 @@ public async Task DeleteAsync(Story item) db.Stories.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -86,4 +86,14 @@ public async Task DeleteAsync(Story item) return -1; } } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/StreamHistoryRepository.cs b/src/SentenceStudio.Shared/Data/StreamHistoryRepository.cs index 82ad82aa..7a9cf199 100644 --- a/src/SentenceStudio.Shared/Data/StreamHistoryRepository.cs +++ b/src/SentenceStudio.Shared/Data/StreamHistoryRepository.cs @@ -58,7 +58,7 @@ public async Task SaveStreamHistoryAsync(StreamHistory streamHistory) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -79,7 +79,7 @@ public async Task DeleteStreamHistoryAsync(StreamHistory streamHistory) db.StreamHistories.Remove(streamHistory); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -119,4 +119,14 @@ public async Task> GetStreamHistoryByVoiceAsync(string voice .OrderByDescending(h => h.CreatedAt) .FirstOrDefaultAsync(); } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/UserActivityRepository.cs b/src/SentenceStudio.Shared/Data/UserActivityRepository.cs index 1f7cd436..1e9b9463 100755 --- a/src/SentenceStudio.Shared/Data/UserActivityRepository.cs +++ b/src/SentenceStudio.Shared/Data/UserActivityRepository.cs @@ -84,7 +84,7 @@ public async Task SaveAsync(UserActivity item) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); // PHASE 2 OPTIMIZATION: Invalidate relevant caches (but NOT TodaysPlan!) // User activities affect vocab summary and practice heat, but not the plan structure @@ -110,7 +110,7 @@ public async Task DeleteAsync(UserActivity item) db.UserActivities.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -120,4 +120,14 @@ public async Task DeleteAsync(UserActivity item) return -1; } } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/UserProfileRepository.cs b/src/SentenceStudio.Shared/Data/UserProfileRepository.cs index 57863d0b..313920d1 100755 --- a/src/SentenceStudio.Shared/Data/UserProfileRepository.cs +++ b/src/SentenceStudio.Shared/Data/UserProfileRepository.cs @@ -302,7 +302,7 @@ public async Task SaveAsync(UserProfile item) int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -328,7 +328,7 @@ public async Task DeleteAsync() db.UserProfiles.RemoveRange(profiles); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -349,7 +349,7 @@ public async Task DeleteAsync(UserProfile item) db.UserProfiles.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -383,4 +383,14 @@ public async Task SaveDisplayCultureAsync(string culture) // Also update the LocalizationManager to reflect changes immediately LocalizationManager.Instance.SetCulture(new CultureInfo(culture)); } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/VocabularyLearningContextRepository.cs b/src/SentenceStudio.Shared/Data/VocabularyLearningContextRepository.cs index 02f2bff2..a38f8f21 100644 --- a/src/SentenceStudio.Shared/Data/VocabularyLearningContextRepository.cs +++ b/src/SentenceStudio.Shared/Data/VocabularyLearningContextRepository.cs @@ -98,7 +98,7 @@ public async Task SaveAsync(VocabularyLearningContext await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return item; } @@ -119,7 +119,7 @@ public async Task DeleteAsync(VocabularyLearningContext item) db.VocabularyLearningContexts.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -129,4 +129,14 @@ public async Task DeleteAsync(VocabularyLearningContext item) return -1; } } + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } + } diff --git a/src/SentenceStudio.Shared/Data/VocabularyProgressRepository.cs b/src/SentenceStudio.Shared/Data/VocabularyProgressRepository.cs index 65ca5156..584757d2 100644 --- a/src/SentenceStudio.Shared/Data/VocabularyProgressRepository.cs +++ b/src/SentenceStudio.Shared/Data/VocabularyProgressRepository.cs @@ -176,7 +176,7 @@ public async Task SaveAsync(VocabularyProgress item) await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return item; } @@ -197,7 +197,7 @@ public async Task DeleteAsync(VocabularyProgress item) db.VocabularyProgresses.Remove(item); int result = await db.SaveChangesAsync(); - _syncService?.TriggerSyncAsync().ConfigureAwait(false); + TriggerSync(); return result; } @@ -423,6 +423,16 @@ public async Task> GetMultipleRe return result; } + + private void TriggerSync() + { + if (_syncService is null) return; + _ = Task.Run(async () => + { + try { await _syncService.TriggerSyncAsync(); } + catch (Exception ex) { _logger.LogError(ex, "Background sync trigger failed"); } + }); + } } ///