Getting Started
Zero to working grain in 15 minutes.
Getting Started
Section titled “Getting Started”Zero to working grain in 15 minutes.
Current API. This guide uses
grainContract/grainFor, typed API records, andFunctionalGrain.ref. Legacy authoring models are documented separately under Legacy API.
What you’ll learn
Section titled “What you’ll learn”- How to define a grain contract and API record with plain F# types — no C# interfaces to write
- How to configure and start a silo
- How to call your grain through a typed API record with
FunctionalGrain.ref - How explicit key codecs keep contract identity stable
Prerequisites
Section titled “Prerequisites”- .NET 10 SDK or later
- A code editor (VS Code + Ionide, Rider, or Visual Studio)
Step 1: Create the project
Section titled “Step 1: Create the project”The template in this repository already uses the current functional API. The currently published
NuGet template 4.1.0 still creates the Legacy CodeGen-based API, so until the next template
release install the current template from a source checkout:
git clone https://github.com/Neftedollar/orleans-fsharp.gitdotnet new install ./orleans-fsharp/templatesdotnet new orleans-fsharp -n MyCountercd MyCounterOr from scratch:
mkdir MyCounter && cd MyCounterdotnet new console -lang F# -n MyCounter.Silocd MyCounter.Silodotnet add package Orleans.FSharpdotnet add package Orleans.FSharp.Runtimedotnet add package Microsoft.Orleans.ServerOrleans.FSharp.Abstractions — the C# assembly the functional runtime’s pre-generated proxies live
in — comes in transitively through Orleans.FSharp; you do not add it, or write a bridge project of
your own, to call a functional grain.
Step 2: Define the contract and API record
Section titled “Step 2: Define the contract and API record”A contract gives your grain a stable wire identity (a grainType string and a key codec); the
API record is a plain F# record of functions describing what you can call. No [<GenerateSerializer>]
or [<Id>] attributes needed anywhere — the built-in FSharpBinaryCodec handles serialization
automatically.
open System.Threading.Tasksopen Orleans.FSharp
type CounterActor = private CounterActor of unit
[<NoEquality; NoComparison>]type CounterApi = { increment: unit -> Task<int> decrement: unit -> Task<int> value: unit -> Task<int> }
[<RequireQualifiedAccess>]module CounterApi = let contract = grainContract<CounterActor, string, CounterApi> { grainType "counter" version 1 stringKey }
let ref = FunctionalGrain.ref contractCounterActor is a phantom brand type — it never gets constructed, it only ties the contract, the
API record, and every FunctionalGrain.ref call site to the same grain identity at compile time.
Every field of CounterApi is one callable operation; its wire ID defaults to the field name.
Step 3: Define the grain
Section titled “Step 3: Define the grain”grainFor { } attaches state and handlers to the contract. Each handler receives the invocation
context, the current state, and the exact argument, and returns (newState, reply):
module Definition = let counterDefinition = grainFor CounterApi.contract { defaultState (fun () -> 0)
handle (_.increment) (fun _context state () -> task { let next = state + 1 return next, next })
handle (_.decrement) (fun _context state () -> task { let next = max 0 (state - 1) return next, next })
handle (_.value) (fun _context state () -> task { return state, state }) }This counter’s state is ephemeral (no stateFrom) — it lives only as long as the activation does.
For durable state, attach addMemoryStorage "provider-name" on the silo plus stateFrom on the
definition; see the persistence model in functional-grains.md.
Step 4: Configure the silo
Section titled “Step 4: Configure the silo”open Orleans.FSharp.Runtime
let config = siloConfig { useLocalhostClustering}useLocalhostClustering runs a single-silo cluster — perfect for local development. siloConfig { }
configures hosting independently from the functional grain definitions.
Step 5: Register the grain and start the host
Section titled “Step 5: Register the grain and start the host”open Microsoft.Extensions.Hostingopen Microsoft.Extensions.DependencyInjectionopen Orleans.FSharpopen Orleans.FSharp.Runtime
[<EntryPoint>]let main _ = let builder = HostApplicationBuilder() SiloConfig.applyToHost config builder
// AddFunctionalGrain is enough for a colocated process: the same IGrainFactory that hosts // the definition also binds its own functional references. A genuinely separate // client-only process would call `clientBuilder.AddFunctionalGrainClient()` instead. builder.UseOrleans(fun siloBuilder -> siloBuilder.AddFunctionalGrain(Definition.counterDefinition) |> ignore) |> ignore
let host = builder.Build() host.Start()
let factory = host.Services.GetRequiredService<Orleans.IGrainFactory>()
// Bind a typed API record — no generated interface required. let api = CounterApi.ref factory "my-counter"
let count1 = (api.increment ()).GetAwaiter().GetResult() printfn "Count after increment = %d" count1
let count2 = (api.value ()).GetAwaiter().GetResult() printfn "Current count = %d" count2
printfn "Silo running. Press Enter to stop." System.Console.ReadLine() |> ignore host.StopAsync().GetAwaiter().GetResult() 0api is a plain CounterApi value — calling api.increment () calls the operation directly, with
no intermediate handle type and no boxed reply to unwrap.
Step 6: Key types at a glance
Section titled “Step 6: Key types at a glance”| Name | Purpose |
|---|---|
grainContract<'Actor,'Key,'Api> { } | Computation expression defining the contract: identity, key codec, per-operation policies |
grainFor contract { } | Computation expression defining state, handlers, persistence, lifecycle hooks, timers, reminders |
FunctionalGrain.ref | Bind a typed API record: IGrainFactory -> 'Key -> 'Api |
FunctionalGrain.rawRef | Bind the typed FunctionalGrainRef wrapper (key, api, call, callCancellable, stream, streamCancellable) |
AddFunctionalGrain | Register a grainFor definition on the silo builder |
AddFunctionalGrainClient | Register the client-side transport on a client-only process |
siloConfig { } | Computation expression to configure the silo |
Step 7: Test it
Section titled “Step 7: Test it”A functional definition keeps your handler as an ordinary function value. A handler that ignores
context is therefore directly callable in a unit test. A handler that reads context (services,
persistent state, grain factory) needs a real activation, since FunctionalGrainContext’s
constructor is internal. See Testing for both patterns, including the full
TestingHost-backed integration-test recipe.
Step 8: Run it
Section titled “Step 8: Run it”dotnet builddotnet run --project MyCounter.Silodotnet testLegacy API
Section titled “Legacy API”Maintaining an older Orleans.FSharp application? Use the isolated Legacy Getting Started guide.
What’s next
Section titled “What’s next”| Guide | Description |
|---|---|
| Functional Grain Runtime | The complete guide to the current authoring model |
| Silo Configuration | Clustering, storage, streaming, security |
| Serialization | FSharpBinaryCodec, JSON fallback, Orleans native |
| Streaming | Publish, subscribe, TaskSeq, broadcast |
| Event Sourcing | journaledGrainFor { } — state as the fold of an event journal, including snapshots |
| Dashboard | Run Orleans Dashboard and inspect functional actor activations |
| Testing | TestingHost integration tests, pure handlers, FsCheck, and log capture |
| API Reference | All public modules and functions |