{"record":{"id":"bc09de5d35f32383","repo":"dotnet/orleans","slug":"recovered-name-does-not-match-written-seriali","errorCode":null,"errorMessage":"Recovered {name} does not match. Written: {Serialize(written)}, recovered: {Serialize(recovered)}.","messagePattern":"Recovered (.+?) does not match\\. Written: (.+?), recovered: (.+?)\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs","lineNumber":140,"sourceCode":"        if (written.Count != recovered.Count)\n        {\n            throw new InvalidOperationException($\"Recovered {name} count does not match. Written: {written.Count}, recovered: {recovered.Count}.\");\n        }\n\n        for (var i = 0; i < written.Count; i++)\n        {\n            if (!equals(written[i], recovered[i]))\n            {\n                throw new InvalidOperationException($\"Recovered {name} item {i} does not match. Written: {Serialize(written[i])}, recovered: {Serialize(recovered[i])}.\");\n            }\n        }\n    }\n\n    private static void EnsureEqual<T>(string name, T written, T recovered, Func<T, T, bool> equals)\n    {\n        if (!equals(written, recovered))\n        {\n            throw new InvalidOperationException($\"Recovered {name} does not match. Written: {Serialize(written)}, recovered: {Serialize(recovered)}.\");\n        }\n    }\n\n    private static bool InventoryEntryEquals(InventoryEntry left, InventoryEntry right)\n        => string.Equals(left.Key, right.Key, StringComparison.Ordinal)\n            && InventoryItemEquals(left.Value, right.Value);\n\n    private static bool InventoryItemEquals(InventoryItem left, InventoryItem right)\n        => string.Equals(left.Sku, right.Sku, StringComparison.Ordinal)\n            && left.Quantity == right.Quantity\n            && left.UnitPrice == right.UnitPrice\n            && left.Attributes.SequenceEqual(right.Attributes, StringComparer.Ordinal);\n\n    private static bool JournalEventEquals(JournalEvent left, JournalEvent right)\n        => string.Equals(left.EventId, right.EventId, StringComparison.Ordinal)\n            && left.Timestamp == right.Timestamp\n            && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal)\n            && left.Notes.SequenceEqual(right.Notes, StringComparer.Ordinal);","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs#L122-L158","documentation":"Thrown by the JournalingAzureBlobJson sample's EnsureEqual<T> helper after it rehydrates a journaled grain and compares the recovered durable state against what was originally written. The sample exercises every IDurable* collection (dictionary, list, queue, set, value) plus persistent state, writes it through an Azure append-blob journal, restarts, and asserts the recovered snapshot is bit-for-bit equal. A mismatch means the journaling codec, ordering, or storage layer did not round-trip the data correctly.","triggerScenarios":"EnsureEqual<T>(name, written, recovered, equals) is invoked when equals(written, recovered) returns false; the per-item overload at line ~140 fires on the first index i where the custom comparer (e.g. InventoryItemEquals, which compares Sku, Quantity, UnitPrice and Attributes.SequenceEqual with an Ordinal comparer) detects a divergence. This runs at the tail end of the sample's recovery phase, after WriteStateAsync on the grain followed by a fresh activation read.","commonSituations":"Editing the sample's grain so the durable state shape changes without re-clearing the blob (stale journal data left from a previous schema). Running against a blob that another sample/activation wrote with different data. A custom serializer or GrainStorageSerializer that is non-idempotent or drops fields. Attribute ordering differences when the comparer is switched away from StringComparer.Ordinal.","solutions":["Re-run with --reset (the default) so the sample wipes and rewrites the append-blob journal before comparing, removing stale-data mismatches.","Inspect the thrown message: it prints Serialize(written) and Serialize(recovered); diff those JSON payloads to find exactly which field diverged.","If you modified InventoryItem/JournalEvent/WorkItem records, make sure [GenerateSerializer] IDs are stable and no [Id] was reordered or removed — changed serializer IDs corrupt recovery.","Ensure the grain calls WriteStateAsync and the second activation is a true fresh activation (DeactivateOnIdle / new silo) so recovery actually reloads from the blob."],"exampleFix":"// before: stale journal data survives across schema changes\n// run with --no-reset against an old blob\nvar resetBlob = !args.Contains(\"--no-reset\", StringComparer.OrdinalIgnoreCase);\n\n// after: force a clean journal rewrite when the schema changed\ndotnet run -- --connection-string ... --reset","handlingStrategy":"validation","validationCode":"// Before reading the summary, confirm the scenario ran and balance was written\nif (balance.Value is null || inventory.Count == 0)\n{\n    Console.WriteLine(\"Scenario has not been run yet; call RunScenario first.\");\n    return;\n}","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Always run RunScenario (which writes balance then WriteStateAsync) before GetSummary.","Run the sample with --reset when the schema changes to avoid stale-journal mismatches.","Keep [GenerateSerializer] / [Id] ordering stable across edits so recovery round-trips."],"tags":["journaling","persistence","azure-blob","durability","verification","sample"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}