Refactor: eliminate triplicated JSON persistence, HTTP timing, and notify-locking patterns#162
Refactor: eliminate triplicated JSON persistence, HTTP timing, and notify-locking patterns#162Copilot wants to merge 3 commits into
Conversation
…patterns - Add JsonFileStore<T> generic thread-safe JSON file persistence helper - Refactor NotificationHistory, ReviewRequestHistory, ReviewRequestService to use JsonFileStore<T> - Extract ExecuteAndNotify(Func<bool>) helper in ReviewRequestService - Extract SendTimedGetAsync(string url) helper in GitHubService Co-authored-by: sunzhuoshi <592211+sunzhuoshi@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors repeated persistence, HTTP timing/logging, and observer-notification locking patterns into shared helpers to reduce duplication across the app.
Changes:
- Introduces
JsonFileStore<T>to centralize thread-safe JSON load/save for multiple history/service classes. - Refactors
ReviewRequestServiceto useExecuteAndNotify(Func<bool>)to unify lock + conditional observer notifications. - Refactors
GitHubServiceto useSendTimedGetAsync(string url)to unify GET timing/logging.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/JsonFileStore.cs | Adds generic JSON file persistence helper used by multiple components. |
| src/NotificationHistory.cs | Switches persistence to JsonFileStore<List<NotificationEntry>>. |
| src/ReviewRequestHistory.cs | Switches persistence to JsonFileStore<List<string>> and rebuilds HashSet from loaded list. |
| src/ReviewRequestService.cs | Delegates persistence to JsonFileStore and centralizes lock+notify via ExecuteAndNotify. |
| src/GitHubService.cs | Centralizes GET timing/logging via SendTimedGetAsync. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var json = File.ReadAllText(_filePath); | ||
| return JsonSerializer.Deserialize<T>(json) ?? defaultFactory(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.LogError($"Error loading {_filePath}", ex); | ||
| } | ||
| } | ||
| return defaultFactory(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Saves <paramref name="data"/> to the JSON file in a thread-safe manner. | ||
| /// </summary> | ||
| public void Save(T data) | ||
| { | ||
| lock (_lockObject) | ||
| { | ||
| try | ||
| { | ||
| var options = new JsonSerializerOptions { WriteIndented = true }; | ||
| var json = JsonSerializer.Serialize(data, options); | ||
| File.WriteAllText(_filePath, json); | ||
| } |
There was a problem hiding this comment.
JsonFileStore<T> creates a new JsonSerializerOptions instance on every Save call and doesn't apply any options on Load. Since this helper is meant to centralize persistence behavior, consider using a single shared JsonSerializerOptions field (and passing it to both Serialize and Deserialize) to avoid option drift and repeated allocations.
| var startTime = DateTime.UtcNow; | ||
| var response = await _httpClient.GetAsync(url); | ||
| var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; | ||
| Logger.LogInfo($"HTTP Response: {(int)response.StatusCode} {response.StatusCode} | {elapsed:F0}ms | {url}"); |
There was a problem hiding this comment.
SendTimedGetAsync measures elapsed time using DateTime.UtcNow deltas, which can be skewed by system clock adjustments (NTP/manual changes) and lead to inaccurate/negative durations. Prefer a monotonic timer like Stopwatch for elapsed timing measurements.
|
@copilot apply changes based on the comments in this thread |
…or timing - JsonFileStore<T>: replace per-call JsonSerializerOptions allocation with a static shared field used for both Serialize and Deserialize - GitHubService.SendTimedGetAsync: replace DateTime.UtcNow delta with Stopwatch for monotonic, clock-skew-resistant elapsed timing Co-authored-by: sunzhuoshi <592211+sunzhuoshi@users.noreply.github.com>
Done in 9ce2cff:
|
src/JsonFileStore.csgeneric thread-safe JSON file persistence helperNotificationHistory.csto useJsonFileStore<T>ReviewRequestHistory.csto useJsonFileStore<T>ReviewRequestService.csto useJsonFileStore<T>and extractExecuteAndNotify(Func<bool>)helperGitHubService.csto extractSendTimedGetAsync(string url)helperJsonSerializerOptionsinJsonFileStore<T>for both Load and SaveStopwatchinstead ofDateTime.UtcNowdeltas inSendTimedGetAsyncOriginal prompt
This section details on the original issue you should resolve
<issue_title>Duplicate Code: JSON File Persistence Pattern Repeated Across History/Service Classes</issue_title>
<issue_description>Analysis of commit ebf44dd
Assignee:
@copilotSummary
Three classes independently implement nearly identical JSON file persistence logic (Load/Save with try/catch and a lock object), and one class repeats an HTTP timing/logging block across three methods. These duplications increase maintenance burden and risk inconsistent bug fixes.
Duplication #1 — JSON File Persistence (Most Significant)
Pattern: Thread-safe JSON Load/Save with identical structure
src/NotificationHistory.cs(lines 17–52,Load/Savemethods)src/ReviewRequestHistory.cs(lines 13–42,Load/Savemethods)src/ReviewRequestService.cs(lines 85–110,Load/Savemethods)Code Sample (nearly identical across all three):
Duplication #2 — HTTP Request Timing/Logging Block
Pattern: Repeated timing measurement around
_httpClient.GetAsyncsrc/GitHubService.csGetCurrentUserAsync(~lines 57–65)GetPendingReviewsAsync(~lines 110–119)GetLatestReleaseAsync(~lines 245–253)Code Sample:
Duplication #3 —
notifyNeededLocking PatternPattern:
bool notifyNeededflag + post-lock observer notificationsrc/ReviewRequestService.csAddOrUpdate(~lines 128–175)MarkAsRead(~lines 177–188)MarkAllAsRead(~lines 190–207)RemoveStaleRequests(~lines 240–255)Code Sample:
Impact Analysis
Loaderror-handling (e.g., adding retry logic) would need to be replicated manually across all three classes.Refactoring Recommendations
For Duplication #1 (JSON Persistence)
Extract a generic
JsonFileStore(T)helper insrc/JsonFileStore.cs:filePathand optionallymaxEntriesT Load()andvoid Save(T data)with the shared try/catch and lockRefactor
NotificationHistory,ReviewRequestHistory, andReviewRequestServiceto delegate load/save toJsonFileStore(T)For Duplication #2 (HTTP Timing)
SendTimedGetAsync(string url)as a private helper inGitHubService.cs:_httpClient.GetAsync, measures elapsed time, logs the resultHttpResponseMessageFor Duplication #3 (notifyNeeded)
ExecuteAndNotify(Action action)helper inReviewRequestService.cs:actioninside a lock, returns bool indicating changeNotifyObservers()if change detectedImplementation Checklist
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.