dotnet/orleans · error · AggregateException

Unable to convert from storage format, Data={value}, DataLen

Error message

Unable to convert from storage format, Data={value}, DataLen={value?.Length ?? -1}, StateType={typeof(T)}

What it means

Thrown by the byte[] overload of ConvertFromStorageFormat<T> when this.serializer.Deserialize<T>(value) fails converting a raw byte[] back into the committed transactional state TState. Like the StateEntity overload it wraps the underlying error in an AggregateException, but its message additionally reports the data length and the target Type, making it the error seen when the committed state itself (not a pending record) cannot be deserialized.

Source

Thrown at src/AWS/Orleans.Transactions.DynamoDB/TransactionalState/DynamoDBTransactionalStateStorage.cs:437

        }

        return dataValue;
    }

    private T ConvertFromStorageFormat<T>(byte[] value)
    {
        T dataValue = default!;

        try
        {
            if (value is { Length: > 0 })
                dataValue = this.serializer.Deserialize<T>(value)!;
        }
        catch (Exception exc)
        {
            var message = $"Unable to convert from storage format, Data={value}, DataLen={value?.Length ?? -1}, StateType={typeof(T)}";
            LogError(logger, message);
            throw new AggregateException(message, exc);
        }

        return dataValue;
    }

    private byte[] ConvertToStorageFormat<T>(T value) => this.serializer.Serialize(value).ToArray();

    private void ValidateKeyItemSize(byte[] metadata, DateTimeOffset timestamp, long committedSequenceId)
    {
        var candidate = new KeyEntity(this.partitionKey)
        {
            CommittedSequenceId = committedSequenceId,
            Metadata = metadata,
            Timestamp = timestamp,
            ETag = long.MaxValue
        };

        ValidateItemSize(candidate.ToStorageFormat(), nameof(metadata));

View on GitHub (pinned to fca799fa70)

Solutions

  1. Read DataLen and StateType from the message, then inspect the persisted bytes for the named partition/sequence to confirm the format.
  2. Make the TState type and the configured IGrainStorageSerializer byte-for-byte compatible with what was written (revert the breaking change or add a version-tolerant reader).
  3. If the committed state is unrecoverable, reset that grain's state (delete its rows in the table) so Load() returns a fresh v0 state.
  4. Never reuse a DynamoDB table/partition across different grain state types; ensure MakePartitionKey inputs (ServiceId, stateName) are stable.

Example fix

// before: state type mutated, breaking older persisted rows
public class MyState { public int Count; }
// later deploy:
public class MyState { public long Count; public string Name; } // incompatible

// after: keep types backward-compatible or migrate via a custom serializer that bridges old->new
public class MyState {
  public long Count { get; set; }
  [JsonExtensionData] public Dictionary<string,object>? Extra { get; set; } // tolerate unknown fields
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Round-trip test the configured serializer against the persisted shape at deploy time
var probe = new TState();
var wire = options.GrainStorageSerializer.Serialize(probe).ToArray();
if (options.GrainStorageSerializer.Deserialize<TState>(wire) is null)
    throw new InvalidOperationException("TState is not round-trippable by the configured serializer");

Type guard

static bool CanReadCommittedState(IGrainStorageSerializer s, byte[] value)
    where TState : class, new()
{
    try { return value is { Length: > 0 } && s.Deserialize<TState>(value) is not null; }
    catch { return false; }
}

Try / catch

try
{
    await grain.ActivateAndLoad();
}
catch (AggregateException ax) when (ax.Message.Contains("Unable to convert from storage format"))
{
    _logger.LogCritical(ax.InnerException, "Committed state deserialization failed; StateType={Type}", typeof(TState));
    // engage recovery: migrate serializer, or reset the grain's rows
    throw;
}

Prevention

When it happens

Trigger: Produced in Load() at line 92 when ConvertFromStorageFormat<TState>(states[pos].Value) is called to materialize the committed state for key.CommittedSequenceId. Triggered when the committed state's stored bytes are incompatible with TState: incompatible type change, switched serializer, null/compression-mangled payload, or a state written by a different TState type that shared the partition key.

Common situations: Changing the grain's state class shape after data exists; switching the storage serializer; pointing a new grain type at a table/partition that contains rows from a different grain type; partial migration of a production table.

Related errors


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