Skip to content

A cancelled or interrupted SaveStateAsync truncates the state file and destroys the previous save; HasSavedStateAsync then says true while LoadStateAsync returns false #71

Description

@matt-edmondson

What's wrong

JsonFilePersistenceProvider.SaveStateAsync writes straight over the live file:

public async Task SaveStateAsync(INavigationState<T> state, CancellationToken cancellationToken = default)
{
Ensure.NotNull(state);
string? directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string json = JsonSerializer.Serialize(state, _jsonOptions);
await File.WriteAllTextAsync(_filePath, json, cancellationToken).ConfigureAwait(false);
}

File.WriteAllTextAsync truncates the file before it writes. If the write is interrupted, the previously good state is already gone. That happens when:

  • the CancellationToken the method accepts is cancelled,
  • the process dies mid-write, or
  • the disk fills up.

LoadStateAsync then hits a JsonException and returns null, so the saved history is lost without any error. Meanwhile HasSavedStateAsync keeps reporting true.

Repro (net10.0)

var provider = new JsonFilePersistenceProvider<NavigationItem>(path);
var nav = new Navigation<NavigationItem>(null, provider);
for (int i = 0; i < 100_000; i++) nav.NavigateTo(new NavigationItem($"p{i}", $"Page {i}"));
await nav.SaveStateAsync();                          // good save
nav.NavigateTo(new NavigationItem("last", "Last"));
using var cts = new CancellationTokenSource();
var t = nav.SaveStateAsync(cts.Token); cts.CancelAfter(1);
try { await t; } catch (OperationCanceledException) { }
var fresh = new Navigation<NavigationItem>(null, provider);
await fresh.LoadStateAsync();

Observed over three runs:

first save ok: 14166702 bytes
second save cancelled
file now: 2809856 bytes          (696320 and 4956160 on the other runs)
LoadStateAsync -> False, Count=0, HasSavedState=True

Suggested fix / acceptance criteria

  • Write to a temporary file in the same directory and flush it, then atomically replace the target (File.Replace, or File.Move(tmp, path, overwrite: true)).
  • Delete the temporary file if the write is cancelled or fails.
  • Test: cancelling a save leaves the previously saved state loadable and unchanged, and leaves no temporary file behind.
  • A successful save still produces the same JSON as today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions