Legacy How To
Section titled “Legacy How To”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 tutorial records the original Orleans.FSharp authoring API for migration. New applications should use the current How To.
Build a distributed system with F# and Microsoft Orleans in under 15 minutes.
This historical guide walks through the former computation-expression model. It is migration reference, not a production-readiness claim.
Historical scope. This tutorial targets the 4.1-and-earlier package shape. It is not tested against the 5.0 package set. For the current grain authoring model (
grainContract/grainFor/FunctionalGrain.ref/AddFunctionalGrain) see functional-grains.md.
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)Historical production checklist
Section titled “Historical production checklist”The archived example referenced these provider areas; availability and support must be re-evaluated against the current functional API:
- 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 Release and Production Status, Silo Configuration, and Security for current guidance.
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