Skip to content

Getting Started

Zero to working grain in 15 minutes.

Zero to working grain in 15 minutes.

Current API. This guide uses grainContract / grainFor, typed API records, and FunctionalGrain.ref. Legacy authoring models are documented separately under Legacy API.

  • 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

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:

Terminal window
git clone https://github.com/Neftedollar/orleans-fsharp.git
dotnet new install ./orleans-fsharp/templates
dotnet new orleans-fsharp -n MyCounter
cd MyCounter

Or from scratch:

Terminal window
mkdir MyCounter && cd MyCounter
dotnet new console -lang F# -n MyCounter.Silo
cd MyCounter.Silo
dotnet add package Orleans.FSharp
dotnet add package Orleans.FSharp.Runtime
dotnet add package Microsoft.Orleans.Server

Orleans.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.Tasks
open 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 contract

CounterActor 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.

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.

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.Hosting
open Microsoft.Extensions.DependencyInjection
open Orleans.FSharp
open 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()
0

api is a plain CounterApi value — calling api.increment () calls the operation directly, with no intermediate handle type and no boxed reply to unwrap.

NamePurpose
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.refBind a typed API record: IGrainFactory -> 'Key -> 'Api
FunctionalGrain.rawRefBind the typed FunctionalGrainRef wrapper (key, api, call, callCancellable, stream, streamCancellable)
AddFunctionalGrainRegister a grainFor definition on the silo builder
AddFunctionalGrainClientRegister the client-side transport on a client-only process
siloConfig { }Computation expression to configure the silo

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.

Terminal window
dotnet build
dotnet run --project MyCounter.Silo
dotnet test

Maintaining an older Orleans.FSharp application? Use the isolated Legacy Getting Started guide.

GuideDescription
Functional Grain RuntimeThe complete guide to the current authoring model
Silo ConfigurationClustering, storage, streaming, security
SerializationFSharpBinaryCodec, JSON fallback, Orleans native
StreamingPublish, subscribe, TaskSeq, broadcast
Event SourcingjournaledGrainFor { } — state as the fold of an event journal, including snapshots
DashboardRun Orleans Dashboard and inspect functional actor activations
TestingTestingHost integration tests, pure handlers, FsCheck, and log capture
API ReferenceAll public modules and functions