Skip to content

Legacy: How To

Archived tutorial for the original Orleans.FSharp authoring API.

This archived tutorial uses the original Orleans.FSharp authoring API. It remains available for maintaining existing applications; new applications should use the current How To.

Build a distributed system with F# and Microsoft Orleans in under 15 minutes.

Orleans.FSharp provides idiomatic F# computation expressions for Microsoft Orleans, the virtual actor framework. This guide walks you through the entire process — from installing the .NET SDK to running a production-ready silo with grains, state persistence, and property-based tests.

Note. This tutorial is written against the grain { } CE, which now carries [<Obsolete>] (warning, not error) — every step still works exactly as written. For the current grain authoring model (grainContract / grainFor / FunctionalGrain.ref / AddFunctionalGrain) see functional-grains.md; the silo, persistence and testing steps are the same under both models.

  • .NET 10 SDK or later
  • A code editor (VS Code with Ionide, JetBrains Rider, or Visual Studio)

Orleans.FSharp ships a dotnet new template that scaffolds a complete solution:

Terminal window
dotnet new install Orleans.FSharp.Templates

Generate a working Orleans.FSharp solution with a silo, grain definitions, and tests:

Terminal window
dotnet new orleans-fsharp -n MyDistributedApp
cd MyDistributedApp

This creates:

  • src/MyDistributedApp.Silo/ — the host process with silo configuration
  • src/MyDistributedApp.Grains/ — grain definitions using grain {} CEs
  • tests/MyDistributedApp.Tests/ — FsCheck property tests with GrainArbitrary

Step 3: Define a grain with discriminated union state

Section titled “Step 3: Define a grain with discriminated union state”

Open the grains project and define your state as an F# discriminated union:

open Orleans
open Orleans.FSharp
[<GenerateSerializer>]
type AccountState =
| [<Id(0u)>] Inactive
| [<Id(1u)>] Active of balance: decimal
[<GenerateSerializer>]
type AccountCommand =
| [<Id(0u)>] Deposit of decimal
| [<Id(1u)>] Withdraw of decimal
| [<Id(2u)>] GetBalance
| [<Id(3u)>] Close

Step 4: Implement the grain with the grain {} computation expression

Section titled “Step 4: Implement the grain with the grain {} computation expression”

Use the grain {} CE to define the grain declaratively — no class inheritance, no mutable state:

let account =
grain {
defaultState Inactive
handle (fun state cmd ->
task {
match state, cmd with
| Inactive, Deposit amount when amount > 0m ->
return Active amount, box amount
| Active balance, Deposit amount when amount > 0m ->
let newBalance = balance + amount
return Active newBalance, box newBalance
| Active balance, Withdraw amount when amount > 0m && amount <= balance ->
let newBalance = balance - amount
if newBalance = 0m then
return Inactive, box 0m
else
return Active newBalance, box newBalance
| Active balance, GetBalance ->
return Active balance, box balance
| Inactive, GetBalance ->
return Inactive, box 0m
| Active _, Close ->
return Inactive, box true
| _ ->
return state, box false
})
persist "Default"
}

The F# compiler ensures every state-command combination is handled. Invalid transitions are caught at compile time, not runtime.

Step 4 (functional equivalent): the current authoring model

Section titled “Step 4 (functional equivalent): the current authoring model”

Step 4 above uses the deprecated grain { } CE. The functional runtime — the current model — gives the same domain a contract (wire identity + key codec + policies) and an API record of typed operations, instead of one boxed message DU:

open System.Threading.Tasks
open Orleans.FSharp
type AccountActor = private AccountActor of unit
[<NoEquality; NoComparison>]
type AccountApi =
{ deposit: decimal -> Task<decimal>
withdraw: decimal -> Task<Result<decimal, string>>
getBalance: unit -> Task<decimal>
close: unit -> Task<bool> }
[<RequireQualifiedAccess>]
module AccountApi =
let contract =
grainContract<AccountActor, string, AccountApi> {
grainType "account"
version 1
stringKey
readOnly (_.getBalance)
}
let ref = FunctionalGrain.ref contract
let accountDefinition =
grainFor AccountApi.contract {
defaultState (fun () -> Inactive)
handle
(_.deposit)
(fun _context state amount ->
task {
if amount <= 0m then
return state, (match state with Active b -> b | Inactive -> 0m)
else
match state with
| Inactive -> return Active amount, amount
| Active balance ->
let next = balance + amount
return Active next, next
})
handle
(_.withdraw)
(fun _context state amount ->
task {
match state with
| Active balance when amount > 0m && amount <= balance ->
let next = balance - amount
if next = 0m then return Inactive, Ok 0m else return Active next, Ok next
| Active _ -> return state, Error "invalid withdrawal amount"
| Inactive -> return state, Error "account is inactive"
})
handle
(_.getBalance)
(fun _context state () -> task { return state, (match state with Active b -> b | Inactive -> 0m) })
handle
(_.close)
(fun _context state () ->
task {
match state with
| Active _ -> return Inactive, true
| Inactive -> return Inactive, false
})
}

Reuses the exact same AccountState DU from Step 3. readOnly (_.getBalance) tells the runtime the handler’s returned state is discarded and lets it interleave with other read-only calls. Register with siloBuilder.AddFunctionalGrain(accountDefinition) on the silo builder (inside the builder.UseOrleans(fun siloBuilder -> ...) delegate), then call it with a typed record instead of a boxed message:

let account = AccountApi.ref factory "account-1"
let! balance = account.deposit 100m
let! result = account.withdraw 40m

See functional-grains.md for the complete guide, including persistence, timers, reminders, and multi-provider writes.

Use the siloConfig {} CE to configure Microsoft Orleans clustering, storage, and streaming:

open Orleans.FSharp.Runtime
let config = siloConfig {
useLocalhostClustering // single-node for development
addMemoryStorage "Default" // in-memory state (swap to Redis/Azure for production)
addDashboard // Orleans Dashboard (map it in your ASP.NET Core pipeline)
}

For production, replace with persistent providers:

let prodConfig = siloConfig {
addRedisClustering redisConnectionString
addRedisStorage "Default" redisConnectionString
addMemoryStreams "StreamProvider"
enableHealthChecks
}
Terminal window
dotnet build
dotnet test
dotnet run --project src/MyDistributedApp.Silo

The silo starts, activates grains on demand, and persists state automatically. Grains are virtual actors — they are always addressable and activated on first call.

Orleans.FSharp includes GrainArbitrary for FsCheck, which auto-generates random command sequences from your DU definition:

open FsCheck
open FsCheck.Xunit
open Orleans.FSharp.Testing
let accountInvariant state =
match state with
| Inactive -> true
| Active balance -> balance > 0m
let applyCommand state cmd =
match state, cmd with
| Inactive, Deposit amount when amount > 0m -> Active amount
| Active balance, Deposit amount when amount > 0m -> Active(balance + amount)
| Active balance, Withdraw amount when amount > 0m && amount <= balance ->
if balance - amount = 0m then Inactive else Active(balance - amount)
| _ -> state
[<Property>]
let ``account balance is never negative`` () =
let arb = GrainArbitrary.forCommands<AccountCommand>()
Prop.forAll arb (fun commands ->
FsCheckHelpers.stateMachineProperty Inactive applyCommand accountInvariant commands)

Publish and subscribe to event streams with typed StreamRef<'T>:

open Orleans.Streams // IStreamProvider
open Orleans.FSharp.Streaming // the Stream module
// In a functional handler: the named provider is a keyed service on context.services
let provider = context.services.GetRequiredKeyedService<IStreamProvider> "StreamProvider"
let stream = Stream.getStream<AccountEvent> provider "Accounts" (string context.key)
do! Stream.publish stream (Deposited amount)

Orleans.FSharp supports all Microsoft Orleans production features:

  • Clustering: Redis, Azure Table Storage, Consul, ZooKeeper, Kubernetes
  • State persistence: Redis, Azure Blob, Cosmos DB, DynamoDB, ADO.NET (SQL Server, PostgreSQL)
  • Streaming: Event Hubs, Azure Queue, memory streams
  • Security: TLS/mTLS, call filters, request context propagation
  • Observability: OpenTelemetry, health checks, Orleans Dashboard

See the Silo Configuration and Security guides for production setup.