refactor: extract JsonFileStore to eliminate triplicated JSON persistence logic#161
refactor: extract JsonFileStore to eliminate triplicated JSON persistence logic#161
Conversation
…ersistence logic Co-authored-by: sunzhuoshi <592211+sunzhuoshi@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors JSON persistence by introducing a shared JsonFileStore helper and updating existing storage classes to use it, reducing duplicated load/save logic across the codebase.
Changes:
- Added
JsonFileStore.Load<T>/JsonFileStore.Save<T>to centralize JSON file persistence and error handling. - Refactored
NotificationHistory,ReviewRequestHistory, andReviewRequestServiceto useJsonFileStoreinstead of inline JSON/file I/O.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/JsonFileStore.cs | New centralized JSON load/save utility used by multiple storage classes. |
| src/NotificationHistory.cs | Replaces inline JSON persistence with JsonFileStore calls. |
| src/ReviewRequestHistory.cs | Replaces inline JSON persistence with JsonFileStore calls and keeps HashSet conversion. |
| src/ReviewRequestService.cs | Replaces inline JSON persistence with JsonFileStore calls for review request details storage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| catch (Exception ex) | ||
| { | ||
| Logger.LogError($"Error loading {filePath}: {ex.Message}", ex); | ||
| } |
There was a problem hiding this comment.
Logger.LogError(message, ex) already appends ex.Message to message when ex is provided (see Logger.LogError). Including {ex.Message} in the message here results in duplicated text in logs. Consider logging "Error loading {filePath}" (and similarly for saving) and rely on Logger to append exception details.
| catch (Exception ex) | ||
| { | ||
| Logger.LogError($"Error saving {filePath}: {ex.Message}", ex); | ||
| } |
There was a problem hiding this comment.
Logger.LogError already appends ex.Message when an exception is passed. Building the message as "Error saving {filePath}: {ex.Message}" will duplicate the exception message in the final log entry. Use a message without ex.Message and pass ex so the logger can format details consistently.
|
@copilot apply changes based on the comments in this thread |
Co-authored-by: sunzhuoshi <592211+sunzhuoshi@users.noreply.github.com>
Done in 0bf0083. Removed |
|
@copilot apply changes based on the comments in this thread |
These changes were already applied in 0bf0083. Both |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| namespace AgentSupervisor | ||
| { | ||
| internal static class JsonFileStore | ||
| { | ||
| public static T Load<T>(string filePath, T defaultValue) | ||
| { | ||
| if (File.Exists(filePath)) | ||
| { | ||
| try | ||
| { | ||
| var json = File.ReadAllText(filePath); | ||
| return JsonSerializer.Deserialize<T>(json) ?? defaultValue; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.LogError($"Error loading {filePath}", ex); | ||
| } | ||
| } | ||
| return defaultValue; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Serializes <paramref name="data"/> to JSON and writes it to <paramref name="filePath"/>. | ||
| /// Callers are responsible for synchronization when concurrent access is possible. | ||
| /// </summary> | ||
| public static void Save<T>(string filePath, T data) | ||
| { | ||
| try | ||
| { | ||
| var options = new JsonSerializerOptions { WriteIndented = true }; | ||
| var json = JsonSerializer.Serialize(data, options); | ||
| File.WriteAllText(filePath, json); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.LogError($"Error saving {filePath}", ex); | ||
| } |
There was a problem hiding this comment.
The XML doc note about synchronization is only on Save(), but Load() can also race with concurrent Save() calls (e.g., read while another thread truncates/writes), leading to JsonException and loss of persisted state. Either extend the doc to cover both methods or centralize synchronization inside JsonFileStore (e.g., per-file lock) so callers can’t accidentally use it unsafely.
| namespace AgentSupervisor | |
| { | |
| internal static class JsonFileStore | |
| { | |
| public static T Load<T>(string filePath, T defaultValue) | |
| { | |
| if (File.Exists(filePath)) | |
| { | |
| try | |
| { | |
| var json = File.ReadAllText(filePath); | |
| return JsonSerializer.Deserialize<T>(json) ?? defaultValue; | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.LogError($"Error loading {filePath}", ex); | |
| } | |
| } | |
| return defaultValue; | |
| } | |
| /// <summary> | |
| /// Serializes <paramref name="data"/> to JSON and writes it to <paramref name="filePath"/>. | |
| /// Callers are responsible for synchronization when concurrent access is possible. | |
| /// </summary> | |
| public static void Save<T>(string filePath, T data) | |
| { | |
| try | |
| { | |
| var options = new JsonSerializerOptions { WriteIndented = true }; | |
| var json = JsonSerializer.Serialize(data, options); | |
| File.WriteAllText(filePath, json); | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.LogError($"Error saving {filePath}", ex); | |
| } | |
| using System.Collections.Concurrent; | |
| namespace AgentSupervisor | |
| { | |
| internal static class JsonFileStore | |
| { | |
| private static readonly ConcurrentDictionary<string, object> FileLocks = new(); | |
| private static object GetFileLock(string filePath) | |
| { | |
| // Use full path to ensure a single lock per physical file. | |
| var fullPath = System.IO.Path.GetFullPath(filePath); | |
| return FileLocks.GetOrAdd(fullPath, _ => new object()); | |
| } | |
| public static T Load<T>(string filePath, T defaultValue) | |
| { | |
| var fileLock = GetFileLock(filePath); | |
| lock (fileLock) | |
| { | |
| if (File.Exists(filePath)) | |
| { | |
| try | |
| { | |
| var json = File.ReadAllText(filePath); | |
| return JsonSerializer.Deserialize<T>(json) ?? defaultValue; | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.LogError($"Error loading {filePath}", ex); | |
| } | |
| } | |
| } | |
| return defaultValue; | |
| } | |
| /// <summary> | |
| /// Serializes <paramref name="data"/> to JSON and writes it to <paramref name="filePath"/>. | |
| /// This method coordinates access with <see cref="Load{T}(string, T)"/> so concurrent | |
| /// reads and writes for the same file path are synchronized. | |
| /// </summary> | |
| public static void Save<T>(string filePath, T data) | |
| { | |
| var fileLock = GetFileLock(filePath); | |
| lock (fileLock) | |
| { | |
| try | |
| { | |
| var options = new JsonSerializerOptions { WriteIndented = true }; | |
| var json = JsonSerializer.Serialize(data, options); | |
| File.WriteAllText(filePath, json); | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.LogError($"Error saving {filePath}", ex); | |
| } | |
| } |
| var options = new JsonSerializerOptions { WriteIndented = true }; | ||
| var json = JsonSerializer.Serialize(data, options); | ||
| File.WriteAllText(filePath, json); |
There was a problem hiding this comment.
JsonSerializerOptions is allocated on every Save() call. Since this helper centralizes JSON persistence, consider using a static readonly JsonSerializerOptions instance to avoid repeated allocations and keep formatting consistent across all callers.
|
@copilot apply changes based on the comments in this thread |
…onFileStore Co-authored-by: sunzhuoshi <592211+sunzhuoshi@users.noreply.github.com>
Done in 6efa1b2. Two changes applied to
|
src/JsonFileStore.cswith generic staticLoad<T>andSave<T>methodsNotificationHistoryto useJsonFileStoreReviewRequestHistoryto useJsonFileStoreReviewRequestServiceto useJsonFileStoreex.Messagein JsonFileStore log messages (Logger already appends it)ConcurrentDictionarylocking inJsonFileStoresoLoadandSaveare both synchronized, preventing races between concurrent reads/writesJsonSerializerOptionsto a static readonly field to avoid per-call allocationOriginal prompt
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.