Appearance
Operations Framework Serialization
The Operations Framework stores operation data in the database using JSON serialization. This document covers how DbOperation and Operation.Items are serialized, and how to customize the serialization.
DbOperation Storage
When an operation is committed, it's stored as a DbOperation entity with the following serialized fields:
| Column | Content | Serializer |
|---|---|---|
ItemsJson | Operation.Items property bag | NewtonsoftJsonSerializer.Default |
CommandJson | The command that triggered the operation | NewtonsoftJsonSerializer.Default |
Default Serializer
DbOperation uses Newtonsoft.Json by default:
cs
// DbOperation.Serializer is a static, mutable property.
// Its default value is NewtonsoftJsonSerializer.Default.
ITextSerializer serializer = DbOperation.Serializer;Newtonsoft.Json is chosen because:
- It handles polymorphic types well with
TypeNameHandling.Auto - It's more forgiving with missing/extra properties during schema evolution
- It has mature support for complex object graphs
Operation.Items Serialization
Operation.Items is a MutablePropertyBag that stores arbitrary key-value pairs. It's serialized to the ItemsJson column:
cs
// How Items are serialized to DbOperation
var ItemsJson = operation.Items.Items.Count == 0
? null
: Serializer.Write(operation.Items.Snapshot, typeof(PropertyBag));PropertyBag Internals
Each item in the bag uses TypeDecoratingUniSerialized<object> to preserve type information:
cs
[DataContract, MemoryPackable, MessagePackObject]
public partial record struct PropertyBagItem(
[property: DataMember] string Key,
[property: DataMember] TypeDecoratingUniSerialized<object> Serialized);This allows heterogeneous values with full type preservation:
cs
// Store different types in the same operation
operation.Items.Set("userId", 123L); // long
operation.Items.Set("metadata", myDto); // custom type
operation.Items.Set("tags", new[] { "a", "b" }); // array
// Types are preserved after serialization round-trip
var userId = operation.Items.Get<long>("userId"); // Works correctly
var metadata = operation.Items.Get<MyDto>("metadata"); // Type preservedCustomizing Serialization
Changing the Default Serializer
To use different serializer settings:
cs
// At application startup, before any operations are processed
DbOperation.Serializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.Auto,
NullValueHandling = NullValueHandling.Ignore,
DateParseHandling = DateParseHandling.None,
// Add custom converters if needed
Converters = { new MyCustomConverter() },
});Using Type-Decorated Serializer
For explicit type information in the JSON:
cs
DbOperation.Serializer = new TypeDecoratingTextSerializer(
new NewtonsoftJsonSerializer(customSettings));This produces JSON like:
json
/* @type MyNamespace.MyCommand, MyAssembly */ {"property": "value"}Command Serialization
Commands stored in CommandJson are serialized with type information to enable proper deserialization during reprocessing:
cs
// Command types must be serializable
[DataContract, MemoryPackable, MessagePackObject]
public sealed partial record CreateTodoCommand(
[property: DataMember, MemoryPackOrder(0), Key(0)] string Title,
[property: DataMember, MemoryPackOrder(1), Key(1)] string? Description
) : ICommand<Todo>;Annotating Command Types
For reliable serialization across Operations Framework, RPC, and other subsystems, annotate commands with all serialization attributes:
cs
[DataContract, MemoryPackable(GenerateType.VersionTolerant), MessagePackObject(true)]
public sealed partial record MyCommand(
[property: DataMember(Order = 0), MemoryPackOrder(0)] string Id,
[property: DataMember(Order = 1), MemoryPackOrder(1)] string Data
) : ICommand<string>
{
[System.Text.Json.Serialization.JsonConstructor, MemoryPackConstructor, SerializationConstructor]
public MyCommand() : this("", "") { }
}Schema Evolution
When evolving command schemas:
- Add new properties as optional with default values
- Don't remove properties from persisted commands (old operations may need reprocessing)
- Don't change property types without a migration strategy
cs
public record CreateUserCommandV1(string Name) : ICommand<User>;cs
public record CreateUserCommandV2(
string Name,
string? Email = null // New optional property
) : ICommand<User>;Deployment Compatibility Contract
DbOperation persists the concrete command (including nested-operation and operation-item types) as polymorphic JSON, and other hosts deserialize it to replay the invalidation pass. This ties command serialization to your deployment process:
- A command type (or any nested-operation/operation-item type it carries) must remain deserializable for at least
MaxEntryAge(30 minutes by default – see Operation Log Trimmer) past its last producer. In practice: don't rename or remove a command type and deploy that change within the same window; stage such changes across releases instead (e.g. keep the old type around, deprecated, for one extra release). - If this contract is violated, deserialization fails on the reading host, and the operation eventually gets abandoned with a single Error-level log once its bounded retry budget is exhausted – it does not fail silently, but it also doesn't self-heal. Losing the invalidation for that operation means dependent caches on that host go stale until something else invalidates them.
- There's no built-in type-alias/rename mapping in the serialization binder today; if a specific rename can't be staged across releases, add one on demand rather than up front.
Troubleshooting
Missing Type Information
If deserialization fails with "Could not determine type", ensure:
- The type is in a loaded assembly
- Type names haven't changed (namespace, class name)
TypeNameHandling.Autois enabled in Newtonsoft.Json settings
PropertyBag Values Not Deserializing
Check that stored types:
- Have parameterless constructors (or appropriate constructor attributes)
- Are public and not internal/private
- Have
[DataContract]or are otherwise serializable
Related Topics
- Core Serialization - General serialization infrastructure
- Operations Framework - Operations Framework overview
- Reprocessing - How operations are replayed
