File.WriteAllでセーブデータの書き込みを行っていると、書き込み中にクラッシュした際にファイルが破損することがある

起こったこと

System.IO.File.WriteAllBytesAsync("SaveData", data); のようなコードでセーブデータの書き込みを行っていました。
しかし、あるときちょうど書き込み中にクラッシュが発生し、SaveDataがサイズは正しいのにすべて0で埋まった状態になってしまいました。
(0埋めになるか0byteになるか、ここはなんでも起こりうる。)

原因

File.WriteAllText / File.WriteAllBytesは既存ファイルに対して上書きする場合、FileMode.Create相当でファイルを開くため
ファイルが存在する場合は内容を0byteに切り詰めてから書き込む(Truncate)挙動をするそうです。
(ここでFileMode.Createで開いているかららしい。)
.NETの内部実装やwin32 apiに疎いので細かい解説は詳しい方にお任せします。
適当なこと書いてるかもしれないのでツッコミください。

解決策

File.Replaceはatomicな操作らしいので、一旦一時ファイルにデータを書き出してからこれで置き換えてやります。
(参考:【C#】File.Replaceはatomicに更新を行うのか?)

Save.cs
var tempPath = Path.Combine(directory, file + ".tmp");
var backupPath = Path.Combine(directory, file + ".bak");

// 1) 一時ファイルへ完全に書く
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
    fs.Write(data, 0, data.Length);

    // 2) OSバッファも含めて可能な限りディスクへ反映
    fs.Flush(flushToDisk: true);
}

// 3) 既存ファイルがあるなら置換、ないなら移動
if (File.Exists(path))
{
    File.Replace(tempPath, path, backupPath, ignoreMetadataErrors: true);
}
else
{
    // 初回作成時は Replace できないので Move
    File.Move(tempPath, path);
}

こうすることで、なるべく安全にファイルを書き換えることができます。
File.Replaceが本当にatomicなら問題ないはずですが、読み込み時も元のファイルを読んでみて失敗したら.bakを読むのが良さそうです。