Legacy Serialization
Section titled “Legacy Serialization”Archived and unsupported. This material is retained only to help migrate existing applications. There is no new Legacy release line, feature or compatibility work, or security fixes. New development must use the current functional API.
This archived page preserves serialization and CodeGen guidance for the original Orleans.FSharp authoring model. Serializer changes which affect persisted data require an explicit migration.
Note.
AddFSharpGrain, thegrain { }CE andGrainDefinition<_,_>referenced below now carry[<Obsolete>](warning, not error). Codec registration works the same way under the functional grain runtime –AddFunctionalGrainalso registersFSharpBinaryCodec– see functional-grains.md.
Quick Comparison
Section titled “Quick Comparison”| Mode | CE Keyword | Speed | C# Project Needed? | Attributes? | Best For |
|---|---|---|---|---|---|
| F# Binary | useFSharpBinarySerialization |
Fast | No | None | Pure F# clusters (recommended) |
| JSON | useFSharpJsonSerialization |
Good | No | None | Readable generalized payloads |
| Orleans Native | (default) | Fastest | Yes (application-owned C# bridge) | [<GenerateSerializer>] + [<Id>] |
Mixed F#/C# clusters |
Universal Grain Pattern — auto-registration
Section titled “Universal Grain Pattern — auto-registration”When you use the universal grain pattern (AddFSharpGrain<State, Command>), F# Binary serialization is registered automatically — you do not need to add useFSharpBinarySerialization to your silo config.
// This is all you need — FSharpBinaryCodec is registered for youbuilder.Services.AddFSharpGrain<CounterState, CounterCommand>(counter) |> ignoreThe registration is idempotent: calling AddFSharpGrain multiple times for different (State, Command) pairs only registers the codec once.
When maintaining the archived model outside the universal pattern with application-owned C# grain
classes, you still need to opt in manually via useFSharpBinarySerialization or
useFSharpJsonSerialization. No 5.0 CodeGen package creates those classes.
Mode 1: F# Binary (Recommended)
Section titled “Mode 1: F# Binary (Recommended)”Binary serialization using FSharp.Reflection — fast, compact, zero boilerplate.
// Your types — plain F#, no attributestype OrderState = | Created of orderId: string | Paid of amount: decimal | Shipped of trackingNo: string | Delivered | Cancelled of reason: string
type OrderCommand = Place of string | Confirm | Ship of string | Cancel of string | GetStatus
// Your grain — cleanlet orderGrain = grain { defaultState (Created "") handle (fun state cmd -> task { ... }) persist "Default"}
// Enable in silo configlet config = siloConfig { useLocalhostClustering addMemoryStorage "Default" useFSharpBinarySerialization // ← this is all you need}How it works: The FSharpBinaryCodecProvider inspects F# types at runtime via FSharp.Reflection, builds binary reader/writer functions, and caches them per type in a ConcurrentDictionary. First access pays the reflection cost (~1ms); subsequent calls are a dictionary lookup (~20ns).
Supported types:
- Discriminated unions (any nesting depth)
- Records
- Options and ValueOptions
- Lists, arrays, sets, maps
- Tuples
- All .NET primitives (int, string, float, decimal, Guid, DateTime, TimeSpan, etc.)
- Byte arrays
- Any nested combination of the above
When to use: Pure F# Orleans clusters. This is the recommended mode for new projects.
Mode 2: F# JSON
Section titled “Mode 2: F# JSON”JSON serialization via FSharp.SystemTextJson — human-readable, flexible schema evolution.
// Same clean types — no attributestype CounterState = { Count: int }type CounterCommand = Increment | Decrement | GetValue
let config = siloConfig { useLocalhostClustering addMemoryStorage "Default" useFSharpJsonSerialization}Pros:
- Human-readable payload (useful for debugging)
- Name-based schema evolution (add/remove fields by name, not ordinal)
- Broad ecosystem compatibility
Cons:
- ~2-5x slower than binary modes
- Larger payload size (text vs binary)
float Infinity,NaNnot supported (IEEE 754 limitation of JSON)option option—Some Noneserializes asnull, deserializes asNone(known limitation)
When to use: Prototyping, debugging, or when you need flexible schema evolution.
Mode 3: Orleans Native
Section titled “Mode 3: Orleans Native”Orleans built-in source-generated serialization — maximum performance, required for C# interop.
// Types need Orleans attributes[<GenerateSerializer>]type CounterState = | [<Id(0u)>] Zero | [<Id(1u)>] Count of int
[<GenerateSerializer>]type CounterCommand = | [<Id(0u)>] Increment | [<Id(1u)>] Decrement | [<Id(2u)>] GetValue
// No serialization keyword needed — it's the defaultlet config = siloConfig { useLocalhostClustering addMemoryStorage "Default"}Requirements:
[<GenerateSerializer>]attribute on every type crossing grain boundaries[<Id(n)>]attribute on every DU case and record field (ordinal position)- An application-owned C# bridge project that references your F# types — the repository’s
Orleans.FSharp.CodeGenproject is archived source, not a 5.0 package - A C# grain class per grain definition (inherits
Grain, delegates to F# handler)
Why so much boilerplate? This historical model put the Orleans-facing class and serializer generation boundary in an application-owned C# project. The retained Orleans.FSharp generator only emits event-sourced stubs; it is not an ordinary-grain bridge generator.
When You MUST Use Orleans Native
Section titled “When You MUST Use Orleans Native”Mixed F#/C# clusters. If your Orleans cluster has both F# silos (using Orleans.FSharp) and C# silos (using standard Orleans), they need to agree on serialization format. Orleans Native is the common format both understand.
F# Silo ←→ C# Silo → Orleans Native (both understand [GenerateSerializer])F# Silo ←→ F# Silo → F# Binary (recommended) or JSONF# Silo only → F# Binary (recommended)Migrating from C# to F#. If you’re gradually moving C# grains to F#, start with Orleans Native for compatibility. Once all silos are F#, switch to F# Binary.
C# core plus new F# grains. Existing C# grains keep Orleans Native serialization. New F# grains can use F# Binary — they have separate state types that don’t cross the C#/F# boundary.
Setting Up an Application-Owned C# Bridge (Orleans Native only, historical)
Section titled “Setting Up an Application-Owned C# Bridge (Orleans Native only, historical)”- Create a C# class library project:
dotnet new classlib -lang C# -n MyApp.CodeGendotnet add MyApp.CodeGen package Microsoft.Orleans.Sdkdotnet add MyApp.CodeGen reference ../MyApp.Grains/MyApp.Grains.fsproj- Add the assembly attribute:
using Orleans;[assembly: GenerateCodeForDeclaringAssembly(typeof(MyApp.Grains.SomeType))]- For each F# grain, create a C# grain class:
[GenerateSerializer]public class CounterGrainImpl : Grain, ICounterGrain{ private readonly GrainDefinition<CounterState, CounterCommand> _def; // ... constructor, HandleMessage delegation to F# handler}- Reference the application-owned C# bridge project from your Silo project.
Mixing Modes
Section titled “Mixing Modes”Generated serializers and one explicit F# policy can coexist. Orleans resolves serializers in priority order:
- Orleans Native (types with
[GenerateSerializer]) — highest priority - The selected F# generalized policy for types without a generated or built-in serializer
This means you can use Orleans Native for shared C#/F# types and F# Binary for F#-only types:
let config = siloConfig { useLocalhostClustering addMemoryStorage "Default" useFSharpBinarySerialization // generalized codec for F#-only types // Orleans Native types still work via [GenerateSerializer]}To use binary first and JSON only for unsupported CLR types, configure one policy instead of enabling two independent flags:
let serialization = FSharpSerialization.Binary |> FSharpSerialization.forUnsupportedTypes FSharpSerialization.Json
let config = siloConfig { useLocalhostClustering useFSharpSerialization serialization}Schema Evolution
Section titled “Schema Evolution”| Scenario | JSON | F# Binary | Orleans Native |
|---|---|---|---|
| Add DU case at end | Works | Works | Works (with new [Id]) |
| Remove DU case | Old data with removed case fails | Same | Same |
| Add record field | Fails (FSharp.SystemTextJson strict) | Fails (ordinal-based) | Fails (ordinal-based) |
| Rename DU case | Fails (name-based) | Works (ordinal-based) | Works (ordinal-based) |
For schema migrations across versions, use the StateMigration module.
Performance
Section titled “Performance”Measured over 10,000 roundtrips of a typical DU with 5 cases:
| Mode | Time | Payload Size | Relative Speed |
|---|---|---|---|
| Orleans Native | ~1ms | Smallest | 1x (baseline) |
| F# Binary | ~2ms | Small | ~2x |
| JSON | ~5ms | Large (text) | ~5x |
All modes are fast enough for real-world Orleans usage. Grain call network latency (~100-500μs) dominates serialization time.
Next Steps
Section titled “Next Steps”- Getting Started — build your first grain
- Legacy: Grain Definition — all 27 deprecated
grain { }CE keywords - Advanced — state migration for schema evolution
- Testing — FsCheck property tests for serialization roundtrips