Skip to content
Orleans.FSharp

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, the grain { } CE and GrainDefinition<_,_> referenced below now carry [<Obsolete>] (warning, not error). Codec registration works the same way under the functional grain runtime – AddFunctionalGrain also registers FSharpBinaryCodec – see functional-grains.md.

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 you
builder.Services.AddFSharpGrain<CounterState, CounterCommand>(counter) |> ignore

The 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.


Binary serialization using FSharp.Reflection — fast, compact, zero boilerplate.

// Your types — plain F#, no attributes
type 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 — clean
let orderGrain = grain {
defaultState (Created "")
handle (fun state cmd -> task { ... })
persist "Default"
}
// Enable in silo config
let 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.

JSON serialization via FSharp.SystemTextJson — human-readable, flexible schema evolution.

// Same clean types — no attributes
type 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, NaN not supported (IEEE 754 limitation of JSON)
  • option optionSome None serializes as null, deserializes as None (known limitation)

When to use: Prototyping, debugging, or when you need flexible schema evolution.

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 default
let 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.CodeGen project 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.

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 JSON
F# 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)”
  1. Create a C# class library project:
Terminal window
dotnet new classlib -lang C# -n MyApp.CodeGen
dotnet add MyApp.CodeGen package Microsoft.Orleans.Sdk
dotnet add MyApp.CodeGen reference ../MyApp.Grains/MyApp.Grains.fsproj
  1. Add the assembly attribute:
AssemblyAttributes.cs
using Orleans;
[assembly: GenerateCodeForDeclaringAssembly(typeof(MyApp.Grains.SomeType))]
  1. For each F# grain, create a C# grain class:
CounterGrainImpl.cs
[GenerateSerializer]
public class CounterGrainImpl : Grain, ICounterGrain
{
private readonly GrainDefinition<CounterState, CounterCommand> _def;
// ... constructor, HandleMessage delegation to F# handler
}
  1. Reference the application-owned C# bridge project from your Silo project.

Generated serializers and one explicit F# policy can coexist. Orleans resolves serializers in priority order:

  1. Orleans Native (types with [GenerateSerializer]) — highest priority
  2. 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
}
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.

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.