Legacy: How To
Archived tutorial for the original Orleans.FSharp authoring API.
Legacy How To
Section titled “Legacy How To”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.
Prerequisites
Section titled “Prerequisites”- .NET 10 SDK or later
- A code editor (VS Code with Ionide, JetBrains Rider, or Visual Studio)
Step 1: Install Orleans.FSharp templates
Section titled “Step 1: Install Orleans.FSharp templates”Orleans.FSharp ships a dotnet new template that scaffolds a complete solution:
dotnet new install Orleans.FSharp.TemplatesStep 2: Create a new project
Section titled “Step 2: Create a new project”Generate a working Orleans.FSharp solution with a silo, grain definitions, and tests:
dotnet new orleans-fsharp -n MyDistributedAppcd MyDistributedAppThis creates:
src/MyDistributedApp.Silo/— the host process with silo configurationsrc/MyDistributedApp.Grains/— grain definitions usinggrain {}CEstests/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 Orleansopen 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)>] CloseStep 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.Tasksopen 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 100mlet! result = account.withdraw 40mSee functional-grains.md for the complete guide, including persistence, timers, reminders, and multi-provider writes.
Step 5: Configure the silo
Section titled “Step 5: Configure the silo”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}Step 6: Build and run
Section titled “Step 6: Build and run”dotnet builddotnet testdotnet run --project src/MyDistributedApp.SiloThe silo starts, activates grains on demand, and persists state automatically. Grains are virtual actors — they are always addressable and activated on first call.
Step 7: Write property-based tests
Section titled “Step 7: Write property-based tests”Orleans.FSharp includes GrainArbitrary for FsCheck, which auto-generates random command sequences from your DU definition:
open FsCheckopen FsCheck.Xunitopen 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)Step 8: Add streaming
Section titled “Step 8: Add streaming”Publish and subscribe to event streams with typed StreamRef<'T>:
open Orleans.Streams // IStreamProvideropen Orleans.FSharp.Streaming // the Stream module
// In a functional handler: the named provider is a keyed service on context.serviceslet provider = context.services.GetRequiredKeyedService<IStreamProvider> "StreamProvider"let stream = Stream.getStream<AccountEvent> provider "Accounts" (string context.key)do! Stream.publish stream (Deposited amount)Step 9: Deploy to production
Section titled “Step 9: Deploy to production”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.
Next steps
Section titled “Next steps”- Functional Grain Runtime — the current authoring model, full guide
- Legacy: Grain Definition — all 27 keywords in the
grain {}CE (deprecated model, kept for reference) - Event Sourcing —
journaledGrainFor { }, a grain whose state is the fold of an event journal - Testing — TestHarness, GrainMock, and property tests
- API Reference — complete module and function reference