dotnet/orleans · error · InvalidOperationException

Balance has not been written.

Error message

Balance has not been written.

What it means

Thrown inside CreateSummary when balance.Value is null — the IDurableValue<AccountBalance> durable value was never assigned before the summary is built. CreateSummary is called from both RunScenario (which sets balance.Value first) and GetSummary, so calling GetSummary on an activation that never ran the scenario trips it.

Source

Thrown at samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs:317

    public Task Deactivate()
    {
        DeactivateOnIdle();
        return Task.CompletedTask;
    }

    private JournaledSampleSummary CreateSummary()
    {
        var completion = receipt.State;
        return new JournaledSampleSummary(
            _activationId,
            inventory.OrderBy(static item => item.Key, StringComparer.Ordinal)
                .Select(static item => new InventoryEntry(item.Key, item.Value))
                .ToArray(),
            events.ToArray(),
            workQueue.ToArray(),
            tags.Order(StringComparer.Ordinal).ToArray(),
            balance.Value ?? throw new InvalidOperationException("Balance has not been written."),
            profile.State,
            completion.Status,
            completion.Value);
    }
}

[GenerateSerializer]
public sealed record JournaledSampleSummary(
    [property: Id(0)] Guid ActivationId,
    [property: Id(1)] InventoryEntry[] Inventory,
    [property: Id(2)] JournalEvent[] Events,
    [property: Id(3)] WorkItem[] WorkQueue,
    [property: Id(4)] string[] Tags,
    [property: Id(5)] AccountBalance Balance,
    [property: Id(6)] ProfileState Profile,
    [property: Id(7)] DurableTaskCompletionSourceStatus CompletionStatus,
    [property: Id(8)] Receipt? Receipt);

View on GitHub (pinned to fca799fa70)

Solutions

  1. Call grain.RunScenario() first — it sets balance.Value before WriteStateAsync — then call GetSummary.
  2. If you want GetSummary to be safe on cold activations, guard with `if (balance.Value is null) return ...` or return an empty/error summary instead of throwing.
  3. Ensure the append blob was not reset between RunScenario and GetSummary, otherwise recovery yields a null balance.

Example fix

// before
var summary = await grain.GetSummary(); // throws on cold activation

// after: run the scenario that initializes balance before summarizing
await grain.RunScenario();
var summary = await grain.GetSummary();
Defensive patterns

Strategy: validation

Validate before calling

// Guard GetSummary-style calls against an unwritten balance
var summary = (balance.Value is null)
    ? null
    : CreateSummary();

Type guard

// Narrow a nullable durable value before use
static bool TryGetBalance(IDurableValue<AccountBalance> balance, out AccountBalance value)
{
    value = balance.Value!;
    return value is not null;
}

Try / catch

null

Prevention

When it happens

Trigger: A client calls grain.GetSummary() on a fresh activation (or one whose RunScenario was never invoked), so balance.Value is still its default null. CreateSummary evaluates `balance.Value ?? throw new InvalidOperationException(...)` and the null-coalesce fires.

Common situations: Restarting the silo against an empty/cleared blob and querying the summary before running the scenario; a second client calling GetSummary while the first has not finished RunScenario; changing the sample flow so balance is written conditionally.

Related errors


AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13). Data as JSON: /api/errors/4b3386c29ab52f18. Report an issue: GitHub.