Idiomatic F# for Microsoft Orleans -- computation expressions, not boilerplate
Orleans is a powerful virtual actor framework, but using it from F# means fighting C# idioms at every turn: mutable state bags, attribute-heavy classes, verbose DI wiring. Orleans.FSharp replaces all of that with computation expressions that let you define grains, configure silos, and wire streaming in natural F# style -- discriminated unions as state, pure handler functions, and declarative configuration. The full Orleans runtime does the heavy lifting underneath.
open Orleans.FSharp
open Orleans.FSharp.Runtime
// 1. Define state and commands — plain F# types, no attributes needed
type CounterState = { Count: int }
type CounterCommand = Increment | Decrement | GetValue
// 2. Define the grain with a computation expression
let counter = grain {
defaultState { Count = 0 }
handle (fun state cmd -> task {
match cmd with
| Increment -> return { Count = state.Count + 1 }, box (state.Count + 1)
| Decrement -> return { Count = state.Count - 1 }, box (state.Count - 1)
| GetValue -> return state, box state.Count
})
persist "Default"
}
// 3. Configure the silo — clean F#, no C# extension method chains
let config = siloConfig {
useLocalhostClustering
addMemoryStorage "Default"
useJsonFallbackSerialization // enables clean types without Orleans attributes
}| Keyword | Description |
|---|---|
defaultState |
Set the initial state value |
handle |
Register a state -> msg -> Task<state * obj> handler |
handleState |
Simpler: state -> msg -> Task<state> — result IS the new state |
handleTyped |
Typed result without manual boxing: state -> msg -> Task<state * 'R> |
handleWithContext |
Handler with GrainContext for grain-to-grain calls and DI |
handleStateWithContext |
GrainContext + state-only result |
handleTypedWithContext |
GrainContext + typed result |
handleWithServices |
Alias for handleWithContext emphasizing DI access |
handleStateWithServices |
Services + state-only result |
handleTypedWithServices |
Services + typed result |
handleCancellable |
Handler with CancellationToken support |
handleStateCancellable |
State-only result + cancellation |
handleTypedCancellable |
Typed result + cancellation |
handleWithContextCancellable |
Context + cancellation |
handleWithServicesCancellable |
Services + cancellation |
persist |
Name the storage provider for state persistence |
additionalState |
Declare a named secondary persistent state |
onActivate |
Hook that runs on grain activation |
onDeactivate |
Hook that runs on grain deactivation |
onReminder |
Register a named reminder handler |
onTimer |
Register a declarative timer with dueTime + period |
onLifecycleStage |
Hook into grain lifecycle stages |
reentrant |
Allow concurrent message processing |
interleave |
Mark a method as always interleaved |
readOnly |
Mark a method as read-only (interleaved for reads) |
mayInterleave |
Custom reentrancy predicate |
statelessWorker |
Allow multiple activations per silo |
maxActivations |
Cap local worker count |
oneWay |
Mark a method as fire-and-forget |
grainType |
Set a custom grain type name |
deactivationTimeout |
Per-grain idle timeout |
implicitStreamSubscription |
Auto-subscribe to a stream namespace |
preferLocalPlacement |
Place grain on the calling silo |
randomPlacement |
Random silo placement |
hashBasedPlacement |
Consistent-hash placement |
activationCountPlacement |
Fewest-activations placement |
resourceOptimizedPlacement |
Resource-aware placement |
siloRolePlacement |
Role-based silo targeting |
customPlacement |
Custom placement strategy type |
| Keyword | Description |
|---|---|
useLocalhostClustering |
Local dev clustering |
addRedisClustering |
Redis-based clustering |
addAzureTableClustering |
Azure Table clustering |
addAdoNetClustering |
ADO.NET clustering (Postgres, SQL Server) |
addMemoryStorage |
In-memory grain storage |
addRedisStorage |
Redis grain storage |
addAzureBlobStorage |
Azure Blob grain storage |
addAzureTableStorage |
Azure Table grain storage |
addAdoNetStorage |
ADO.NET grain storage |
addCosmosStorage |
Cosmos DB grain storage |
addDynamoDbStorage |
DynamoDB grain storage |
addCustomStorage |
Custom storage provider |
addMemoryStreams |
In-memory stream provider |
addPersistentStreams |
Durable stream provider |
addBroadcastChannel |
Broadcast channel provider |
addMemoryReminderService |
In-memory reminders |
addRedisReminderService |
Redis reminders |
addCustomReminderService |
Custom reminder service |
useSerilog |
Wire Serilog as logging provider |
configureServices |
Register custom DI services |
addIncomingFilter |
Incoming grain call filter |
addOutgoingFilter |
Outgoing grain call filter |
addGrainService |
Register a GrainService type |
addStartupTask |
Run a task when the silo starts |
enableHealthChecks |
Register health check endpoints |
useTls / useTlsWithCertificate |
TLS encryption |
useMutualTls / useMutualTlsWithCertificate |
Mutual TLS |
addDashboard / addDashboardWithOptions |
Orleans Dashboard |
useGrainVersioning |
Grain interface versioning |
clusterId / serviceId / siloName |
Cluster identity |
siloPort / gatewayPort / advertisedIpAddress |
Endpoints |
grainCollectionAge |
Global idle deactivation timeout |
| Keyword | Description |
|---|---|
useLocalhostClustering |
Local dev clustering |
useStaticClustering |
Static gateway endpoints |
addMemoryStreams |
In-memory stream provider |
configureServices |
Register custom DI services |
useTls / useTlsWithCertificate |
TLS encryption |
useMutualTls |
Mutual TLS |
clusterId / serviceId |
Cluster identity |
gatewayListRefreshPeriod |
Gateway refresh interval |
preferredGatewayIndex |
Preferred gateway |
Call any registered F# grain without defining a per-grain C# interface:
// Silo startup — register your grain definition
siloBuilder.Services.AddFSharpGrain<PingState, PingCommand>(pingGrain) |> ignore
// Client / handler — string, GUID, or int key
let handle = FSharpGrain.ref<PingState, PingCommand> factory "ping-1"
let! state = handle |> FSharpGrain.send Ping // returns Task<PingState>
do! handle |> FSharpGrain.post Ping // awaits RPC but discards result
// ask returns a type you choose — useful when the handler returns something other than the state
let! count = handle |> FSharpGrain.ask<PingState, PingCommand, int> GetCount
// GUID and integer keys
let h = FSharpGrain.refGuid<S, M> factory (Guid.NewGuid())
let! s = h |> FSharpGrain.sendGuid MyCommand
let! r = h |> FSharpGrain.askGuid<S, M, string> QueryCmd
let h = FSharpGrain.refInt<S, M> factory 42L
do! h |> FSharpGrain.postInt MyCommandThe universal pattern works with any F# discriminated union as the command type — including cases with fields (Append of string) and nullary cases in mixed DUs. No CodeGen project is required; Orleans discovers the grains through Orleans.FSharp.Abstractions.
| Keyword | Description |
|---|---|
defaultState |
Initial state before any events |
apply |
Pure event fold: state -> event -> state |
handle |
Command handler: state -> command -> event list |
logConsistencyProvider |
Orleans log consistency provider name |
dotnet add package Orleans.FSharp
dotnet add package Orleans.FSharp.RuntimeSilo-side proxy generation (required for Orleans to locate your grains):
dotnet add package Orleans.FSharp.AbstractionsOptional packages:
dotnet add package Orleans.FSharp.EventSourcing # Event sourcing
dotnet add package Orleans.FSharp.Testing # Test harness + FsCheckWhy
Orleans.FSharp.Abstractions? Orleans source generators only run on C# projects.Abstractionsis a tiny C# shim (no code to write) that lets the Orleans runtime generate the proxy classes forIFSharpGrain. Reference it from your silo project — that's it.
Scaffold a new project in seconds:
dotnet new install Orleans.FSharp.Templates
dotnet new orleans-fsharp -n MyApp| Guide | Description |
|---|---|
| Getting Started | Zero to working grain in 15 minutes |
| Grain Definition | Complete grain { } CE reference |
| Silo Configuration | Complete siloConfig { } CE reference |
| Client Configuration | clientConfig { } CE reference |
| Serialization | 3 modes: F# Binary, JSON, Orleans Native |
| Streaming | Publish, subscribe, TaskSeq, broadcast |
| Event Sourcing | eventSourcedGrain { } CE guide |
| Testing | TestHarness, FsCheck, GrainMock |
| Analyzers | OF0001: async {} detection, AllowAsync opt-out |
| Security | TLS, mTLS, filters, secrets |
| Advanced | Transactions, telemetry, shutdown, migration |
| Resilience | Polly v8 retry, circuit-breaker, and timeout patterns |
| Redis Example | End-to-end shopping cart with Redis storage/clustering |
| API Reference | All public modules, types, functions |
| Package | Description |
|---|---|
Orleans.FSharp |
Core: grain CE, GrainRef, streaming, logging, reminders, timers, observers, serialization |
Orleans.FSharp.Runtime |
Silo hosting, client config, grain discovery |
Orleans.FSharp.Abstractions |
C# shim — Orleans proxy generation for IFSharpGrain (reference from silo) |
Orleans.FSharp.EventSourcing |
Event-sourced grain CE |
Orleans.FSharp.CodeGen |
Optional: per-grain C# code generation for custom grain interfaces (legacy pattern) |
Orleans.FSharp.Testing |
Test harness, GrainArbitrary, GrainMock, log capture |
Orleans.FSharp.Analyzers |
F# analyzer: OF0001 warns on async { } usage; [<AllowAsync>] opt-out |
Orleans.FSharp.Templates |
dotnet new project template |
Never inline connection strings containing passwords or secrets in source code. Load them from configuration or environment variables at runtime.
Recommended: Use IConfiguration or environment variables:
let connStr = Environment.GetEnvironmentVariable("REDIS_CONNECTION")
let config = siloConfig {
useLocalhostClustering
addRedisStorage "Default" connStr
}Avoid: Hardcoding secrets in source files:
// DO NOT do this -- secrets will leak into version control
addRedisStorage "Default" "redis://user:password@host:6379"When using useTls or useMutualTls, always use valid certificates from a trusted certificate authority in production. Do not disable certificate validation in production environments.
Contributions are welcome! Please open an issue or pull request on GitHub.
This project is licensed under the MIT License.