What's wrong
When the file can't be deserialized, AppData<T>.LoadOrCreate does this (AppDataStorage/AppData.cs):
catch (JsonException)
{
// file was corrupt or could not be deserialized
// delete and try load a backup
AppData.FileSystem.File.Delete(newAppData.FilePath);
return LoadOrCreate(subdirectory, fileName);
}
The comment says it deletes the file and then loads a backup, but in normal operation no backup exists. WriteText removes the .bk file as the last step of every successful save (FileSystem.File.Delete(bkFilePath);). A .bk or .tmp only survives a process that crashed mid-save. So in practice, once the file fails to deserialize, the user's only copy of their data is permanently deleted, and a fresh default instance is saved over the same path.
TryRestoreFrom already archives the recovery candidates it promotes ("Archiving rather than deleting keeps the recovered content for inspection"). The main file gets no such protection.
Repro
Run against the current main source with MockFileSystem:
LoadOrCreate(), set Data = "years of user settings", Save(). The directory now holds only test_app_data.json, with no .bk.
- Make a one-character hand edit that breaks the JSON (a stray
,).
LoadOrCreate() returns Data = "". The directory holds only test_app_data.json, which now contains the defaults. No file anywhere contains the original text.
Why it matters
A JsonException is not just a sign of disk corruption. Ordinary things raise it:
- An app update changes the model. For example, it renames or removes an enum member (
JsonStringEnumConverter throws on an unknown name) or changes a property's type. The first launch of the new version silently resets every user's settings to defaults, and the old file is gone, so the developer can't write a migration after the fact.
- The user hand-edits a settings file and leaves a trailing comma. All their settings are wiped instead of being kept for them to fix.
In both cases, nothing is logged or thrown, and nothing is left on disk to recover from.
Suggested fix
- Replace
File.Delete with a move to an archived name, such as <file>.corrupt.<yyyyMMdd_HHmmss>, deduplicated with the same counter loop TryRestoreFrom uses. The recursive LoadOrCreate still finds the main file missing and falls back to the temp, then the backup, then defaults. The unreadable content stays on disk for the user or a migration to recover.
- Optionally, expose the archive (an event, or a property with the archived path) so an app can tell the user that its settings were reset and where the old file went.
- Correct the comment, since in the common case no backup exists.
Acceptance: after the repro above, the directory contains the defaults file plus an archived copy with the original (unparseable) content. The existing TestLoadOrCreateHandlesCorruptFile and recursive-recovery tests still pass.
What's wrong
When the file can't be deserialized,
AppData<T>.LoadOrCreatedoes this (AppDataStorage/AppData.cs):The comment says it deletes the file and then loads a backup, but in normal operation no backup exists.
WriteTextremoves the.bkfile as the last step of every successful save (FileSystem.File.Delete(bkFilePath);). A.bkor.tmponly survives a process that crashed mid-save. So in practice, once the file fails to deserialize, the user's only copy of their data is permanently deleted, and a fresh default instance is saved over the same path.TryRestoreFromalready archives the recovery candidates it promotes ("Archiving rather than deleting keeps the recovered content for inspection"). The main file gets no such protection.Repro
Run against the current
mainsource withMockFileSystem:LoadOrCreate(), setData = "years of user settings",Save(). The directory now holds onlytest_app_data.json, with no.bk.,).LoadOrCreate()returnsData = "". The directory holds onlytest_app_data.json, which now contains the defaults. No file anywhere contains the original text.Why it matters
A
JsonExceptionis not just a sign of disk corruption. Ordinary things raise it:JsonStringEnumConverterthrows on an unknown name) or changes a property's type. The first launch of the new version silently resets every user's settings to defaults, and the old file is gone, so the developer can't write a migration after the fact.In both cases, nothing is logged or thrown, and nothing is left on disk to recover from.
Suggested fix
File.Deletewith a move to an archived name, such as<file>.corrupt.<yyyyMMdd_HHmmss>, deduplicated with the same counter loopTryRestoreFromuses. The recursiveLoadOrCreatestill finds the main file missing and falls back to the temp, then the backup, then defaults. The unreadable content stays on disk for the user or a migration to recover.Acceptance: after the repro above, the directory contains the defaults file plus an archived copy with the original (unparseable) content. The existing
TestLoadOrCreateHandlesCorruptFileand recursive-recovery tests still pass.