Grain Definition
Complete guide to the grain computation expression.
Grain Definition
Section titled “Grain Definition”Complete guide to the grain { } computation expression.
What you’ll learn
Section titled “What you’ll learn”- Every keyword in the
grain { }CE - How to define handlers, lifecycle hooks, reminders, and timers
- Allowing message types to interleave with
interleaveMessage - Multiple named states, and where per-grain Orleans attributes live (the C# CodeGen path)
Overview
Section titled “Overview”The grain { } CE builds a GrainDefinition<'State, 'Message> — an immutable record that fully describes a grain’s behavior. The Orleans.FSharp.CodeGen package reads this definition at build time and generates the corresponding C# grain class with all the correct Orleans attributes.
open Orleans.FSharp
let myGrain = grain { defaultState initialValue handle handlerFunction persist "StorageProviderName"}Every grain definition requires at minimum:
- A
defaultState— the initial state value - At least one handler from the table below
Handler variant quick reference
Section titled “Handler variant quick reference”| Variant | Signature | Use when |
|---|---|---|
handle | 'S -> 'M -> Task<'S * obj> | Full control, manual box |
handleState | 'S -> 'M -> Task<'S> | Caller only needs state |
handleTyped | 'S -> 'M -> Task<'S * 'R> | Typed result, no box |
handleWithContext | GrainContext -> 'S -> 'M -> Task<'S * obj> | Need grain-to-grain calls or DI |
handleStateWithContext | GrainContext -> 'S -> 'M -> Task<'S> | Context + state-only return |
handleTypedWithContext | GrainContext -> 'S -> 'M -> Task<'S * 'R> | Context + typed result |
handleCancellable | 'S -> 'M -> CancellationToken -> Task<'S * obj> | Long-running ops, manual box |
handleStateCancellable | 'S -> 'M -> CancellationToken -> Task<'S> | Long-running ops, state-only |
handleTypedCancellable | 'S -> 'M -> CancellationToken -> Task<'S * 'R> | Long-running ops, typed result |
handleWithContextCancellable | GrainContext -> 'S -> 'M -> CancellationToken -> Task<'S * obj> | Context + cancellation |
handleStateWithContextCancellable | GrainContext -> 'S -> 'M -> CancellationToken -> Task<'S> | Context + cancellation, state-only |
handleTypedWithContextCancellable | GrainContext -> 'S -> 'M -> CancellationToken -> Task<'S * 'R> | Context + cancellation, typed result |
Aliases: handleWithServices = handleWithContext, handleStateWithServices = handleStateWithContext,
handleWithServicesCancellable = handleWithContextCancellable,
handleStateWithServicesCancellable = handleStateWithContextCancellable,
handleTypedWithServicesCancellable = handleTypedWithContextCancellable.
State and Handlers
Section titled “State and Handlers”defaultState
Section titled “defaultState”Sets the initial state of the grain when first activated. Required for every grain.
let myGrain = grain { defaultState 0 handle (fun state msg -> task { return state + 1, box(state + 1) })}For DU state:
let myGrain = grain { defaultState Zero handle (fun state msg -> task { return Count 1, box 1 })}handle
Section titled “handle”Registers the message handler. Takes the current state and a message, returns a Task<'State * obj> (new state and a boxed result).
grain { defaultState 0 handle (fun state msg -> task { match msg with | Add n -> return state + n, box(state + n) | Get -> return state, box state })}handleState
Section titled “handleState”A simpler variant of handle for grains where the caller only needs the updated state. The handler returns Task<'State> — no need to box a result separately.
grain { defaultState { Count = 0 } handleState (fun state msg -> task { match msg with | Increment -> return { Count = state.Count + 1 } | Decrement -> return { Count = state.Count - 1 } | GetValue -> return state })}The result returned to the caller is the new state (boxed internally).
handleTyped
Section titled “handleTyped”Like handle, but accepts a strongly-typed result instead of obj. No manual box call needed.
grain { defaultState { Count = 0 } handleTyped (fun state msg -> task { match msg with | Increment -> let ns = { Count = state.Count + 1 } return ns, ns.Count // 'Result = int — no box needed | GetValue -> return state, state.Count })}The 'Result type is inferred from the handler return type. The framework boxes it internally before sending to the caller.
handleWithContext
Section titled “handleWithContext”Like handle, but the handler receives a GrainContext as the first argument. Use this when you need to call other grains or resolve DI services from within the handler.
grain { defaultState Map.empty handleWithContext (fun ctx state msg -> task { match msg with | Aggregate key -> let otherGrain = GrainContext.getGrainByString<IOtherGrain> ctx key let! value = GrainRef.invoke otherGrain (fun g -> g.GetValue()) return state |> Map.add key value, box value })}The GrainContext provides:
| Member | Description |
|---|---|
GrainContext.getService<'T> | Resolve a DI service |
GrainContext.getState<'T> name | Get a named additional persistent state |
GrainContext.getGrainByString<'T> key | Get a grain ref by string key |
GrainContext.getGrainByGuid<'T> key | Get a grain ref by GUID key |
GrainContext.getGrainByInt64<'T> key | Get a grain ref by int64 key |
GrainContext.getGrainByGuidCompound<'T> guid ext | Compound GUID+string key |
GrainContext.getGrainByIntCompound<'T> key ext | Compound int64+string key |
GrainContext.deactivateOnIdle | Request deactivation when idle |
GrainContext.delayDeactivation span | Delay deactivation |
GrainContext.grainId | Get the GrainId |
GrainContext.primaryKeyString | Get the string primary key |
GrainContext.primaryKeyGuid | Get the Guid primary key |
GrainContext.primaryKeyInt64 | Get the int64 primary key |
handleStateWithContext
Section titled “handleStateWithContext”Like handleWithContext, but returns only the new state — no need to manually box a result. Use this when the handler needs GrainContext but the caller only needs the updated state.
grain { defaultState { Score = 0 } handleStateWithContext (fun ctx state msg -> task { match msg with | AddPoints n -> return { Score = state.Score + n } | SubtractPoints n -> return { Score = state.Score - n } | NotifyAchievement achievementId -> let notifier = GrainContext.getService<IAchievementNotifier> ctx do! notifier.NotifyAsync(achievementId) return state })}Pair with
FSharpGrain.sendto receive the updated state back.
handleTypedWithContext
Section titled “handleTypedWithContext”Like handleWithContext, but returns a strongly-typed result — no manual box call needed. Combines context access with the clean handleTyped return style.
grain { defaultState { Balance = 0m } handleTypedWithContext (fun ctx state msg -> task { match msg with | Deposit amount -> let ns = { Balance = state.Balance + amount } return ns, ns.Balance // 'Result = decimal | GetBalance -> return state, state.Balance })}Pair with
FSharpGrain.ask<'S,'C,decimal>to receive the typed result.
handleWithServices
Section titled “handleWithServices”Alias for handleWithContext that emphasizes DI access. Identical behavior.
grain { defaultState [] handleWithServices (fun ctx state msg -> task { let logger = GrainContext.getService<ILogger<_>> ctx Log.logInfo logger "Processing {Command}" [| box msg |] return state, box () })}handleCancellable
Section titled “handleCancellable”Like handle, but the handler receives a CancellationToken for cooperative cancellation of long-running operations.
grain { defaultState "" handleCancellable (fun state msg ct -> task { let! result = longRunningOperation ct return result, box result })}handleStateCancellable
Section titled “handleStateCancellable”Like handleState, but the handler receives a CancellationToken. Returns only the updated state — no manual box needed.
grain { defaultState { Items = [] } handleStateCancellable (fun state msg ct -> task { match msg with | FetchAndAppend url -> let! item = fetchWithTimeout url ct return { Items = item :: state.Items } | Clear -> return { Items = [] } })}The CancellationToken comes from the Orleans runtime and allows long-running fetch/IO operations to be cancelled cleanly when the silo shuts down.
handleTypedCancellable
Section titled “handleTypedCancellable”Like handleTyped, but the handler also receives a CancellationToken. Returns a strongly-typed result — no box needed.
grain { defaultState { Results = [] } handleTypedCancellable (fun state msg ct -> task { match msg with | ComputeSum inputs -> let sum = List.sum inputs return { Results = sum :: state.Results }, sum // 'Result = int | GetLatest -> let latest = state.Results |> List.tryHead |> Option.defaultValue 0 return state, latest })}Pair with
FSharpGrain.ask<'S,'C,int>to receive the typed result.
handleWithContextCancellable
Section titled “handleWithContextCancellable”Combines GrainContext and CancellationToken.
grain { defaultState 0 handleWithContextCancellable (fun ctx state msg ct -> task { let httpClient = GrainContext.getService<HttpClient> ctx let! response = httpClient.GetAsync("https://api.example.com", ct) return state + 1, box response.StatusCode })}handleWithServicesCancellable
Section titled “handleWithServicesCancellable”Alias for handleWithContextCancellable.
handleStateWithContextCancellable
Section titled “handleStateWithContextCancellable”Combines GrainContext, CancellationToken, and state-only return — no manual box needed. The maximum set of inputs with the simplest return.
grain { defaultState { Items = [] } handleStateWithContextCancellable (fun ctx state msg ct -> task { match msg with | FetchAndStore url -> let http = GrainContext.getService<HttpClient> ctx let! body = http.GetStringAsync(url, ct) return { Items = body :: state.Items } | Clear -> return { Items = [] } })}handleStateWithServicesCancellable
Section titled “handleStateWithServicesCancellable”Alias for handleStateWithContextCancellable.
handleTypedWithContextCancellable
Section titled “handleTypedWithContextCancellable”The full combination: GrainContext, CancellationToken, and a typed result — no box needed.
grain { defaultState { Total = 0 } handleTypedWithContextCancellable (fun ctx state msg ct -> task { match msg with | FetchAndAdd url -> let http = GrainContext.getService<HttpClient> ctx let! n = http.GetStringAsync(url, ct) |> Task.map int return { Total = state.Total + n }, state.Total + n // 'Result = int | GetTotal -> return state, state.Total })}Pair with
FSharpGrain.ask<'S,'C,int>to receive the typed result.
handleTypedWithServicesCancellable
Section titled “handleTypedWithServicesCancellable”Alias for handleTypedWithContextCancellable.
Persistence
Section titled “Persistence”persist
Section titled “persist”Names the Orleans storage provider for state persistence. The provider must be registered in the silo configuration.
grain { defaultState Zero handle (fun state msg -> task { return Count 1, box 1 }) persist "Default"}Without persist, the grain state is in-memory only and is lost on deactivation.
additionalState
Section titled “additionalState”Declares a named secondary persistent state that can be accessed via GrainContext.getState. Use this when a grain needs multiple independently persisted state values.
grain { defaultState { Balance = 0m }
additionalState "AuditLog" "AuditStore" ([] : AuditEntry list)
handleWithContext (fun ctx state msg -> task { let auditState = GrainContext.getState<AuditEntry list> ctx "AuditLog" let currentAudit = auditState.State // ... process and update both states return newState, box result })
persist "Default"}Parameters: additionalState name storageName defaultValue
Lifecycle Hooks
Section titled “Lifecycle Hooks”onActivate
Section titled “onActivate”Runs when the grain is activated. Receives the current state and returns a potentially modified state.
grain { defaultState { Cache = Map.empty; LastRefresh = DateTime.MinValue } handle myHandler persist "Default"
onActivate (fun state -> task { printfn "Grain activated with state: %A" state return { state with LastRefresh = DateTime.UtcNow } })}onDeactivate
Section titled “onDeactivate”Runs when the grain is being deactivated. Receives the current state for cleanup.
grain { defaultState { ConnectionId = None } handle myHandler persist "Default"
onDeactivate (fun state -> task { match state.ConnectionId with | Some id -> printfn "Closing connection %s" id | None -> () })}onLifecycleStage
Section titled “onLifecycleStage”Hooks into Orleans grain lifecycle stages for fine-grained control. Standard stages:
| Stage | Value | Description |
|---|---|---|
GrainLifecycleStage.First | 2000 | First stage after creation |
GrainLifecycleStage.SetupState | 4000 | State setup |
GrainLifecycleStage.Activate | 6000 | Activation |
GrainLifecycleStage.Last | int.MaxValue | Final stage |
grain { defaultState 0 handle myHandler
onLifecycleStage 2000 (fun ct -> task { printfn "Early lifecycle hook firing" })
onLifecycleStage 6000 (fun ct -> task { printfn "Activation-stage hook firing" })}Multiple hooks at the same stage are executed in registration order.
Reminders and Timers
Section titled “Reminders and Timers”onReminder
Section titled “onReminder”Registers a named reminder handler. Reminders are persistent periodic triggers that survive grain deactivation and silo restarts. The silo must have a reminder service configured.
grain { defaultState { CheckCount = 0 } handle myHandler persist "Default"
onReminder "HealthCheck" (fun state reminderName tickStatus -> task { printfn "Reminder %s fired at %A" reminderName tickStatus.CurrentTickTime return { state with CheckCount = state.CheckCount + 1 } })}To register the reminder at runtime, use the Reminder module:
Reminder.register grain "HealthCheck" (TimeSpan.FromMinutes 1.) (TimeSpan.FromMinutes 5.)onTimer
Section titled “onTimer”Registers a declarative timer that is automatically started on grain activation and stopped on deactivation. Timers are in-memory only — they do not survive deactivation.
grain { defaultState { HeartbeatCount = 0 } handle myHandler
onTimer "Heartbeat" (TimeSpan.FromSeconds 10.) // dueTime: first fire after 10s (TimeSpan.FromSeconds 30.) // period: then every 30s (fun state -> task { return { state with HeartbeatCount = state.HeartbeatCount + 1 } })}Reentrancy and Concurrency
Section titled “Reentrancy and Concurrency”By default each grain processes one message at a time. Under the universal grain pattern the
one concurrency lever is interleaveMessage.
interleaveMessage
Section titled “interleaveMessage”Registers a message type whose calls may interleave with other calls on the same grain — the
working replacement for the removed string-based mayInterleave keyword.
grain { defaultState Map.empty handle myHandler interleaveMessage typeof<Query> // allow all Query cases to interleave}All F# grains share Orleans.FSharp.FSharpGrainImpl, which carries a single class-level
[MayInterleave] predicate keyed on the incoming message’s runtime type. Each registered type
is pushed into the process-wide FSharpInterleaveRegistry when the grain is added to the silo.
Pass the discriminated-union type itself — the predicate matches by assignability, so
field-carrying DU cases (which compile to nested subtypes that cannot be named in source) are
covered. Repeated registrations of the same type are de-duplicated.
Caution: registering a broad base type, interface, or
objwill make every assignable message type interleavable — always register specific DU or message types.
Per-grain Orleans attributes (C# CodeGen path)
Section titled “Per-grain Orleans attributes (C# CodeGen path)”Reentrancy beyond interleaveMessage, stateless workers, placement strategies, one-way and
read-only methods, implicit stream subscriptions, and custom grain-type names are not
grain { } CE keywords. The universal grain pattern shares a single FSharpGrainImpl class and
one handler method, so per-grain class-level or per-method attributes cannot be expressed there.
To use them, define the grain through the per-grain Orleans.FSharp.CodeGen path: each grain
compiles to its own C# class/method that carries the real Orleans attribute.
| Concept | Orleans attribute (CodeGen path) |
|---|---|
| Reentrant grain | [Reentrant] |
| Custom interleave predicate | [MayInterleave("Predicate")] |
| Always-interleaved / read-only method | [AlwaysInterleave] / [ReadOnly] |
| Stateless worker (+ max activations) | [StatelessWorker] / [StatelessWorker(n)] |
| Fire-and-forget method | [OneWay] |
| Custom grain type name | [GrainType("name")] |
| Implicit stream subscription | [ImplicitStreamSubscription("namespace")] |
| Placement | [PreferLocalPlacement], [RandomPlacement], [HashBasedPlacement], [ActivationCountBasedPlacement], [ResourceOptimizedPlacement], [SiloRoleBasedPlacement], or a custom PlacementStrategy |
Per-grain idle deactivation timeouts are configured globally with
grainCollectionAgeinsiloConfig { }; there is no per-graingrain { }keyword for it.
Complete Example
Section titled “Complete Example”Here is a grain that uses many features together:
open Systemopen System.Threading.Tasksopen Orleans.FSharp
[<GenerateSerializer>]type ChatState = | [<Id(0u)>] Empty | [<Id(1u)>] Active of messages: string list * participants: Set<string>
[<GenerateSerializer>]type ChatCommand = | [<Id(0u)>] Join of user: string | [<Id(1u)>] Leave of user: string | [<Id(2u)>] Send of user: string * text: string | [<Id(3u)>] GetHistory | [<Id(4u)>] GetParticipants
let chatRoom = grain { defaultState Empty
handleWithContext (fun ctx state cmd -> task { match state, cmd with | Empty, Join user -> let newState = Active([], Set.singleton user) return newState, box true | Active(msgs, users), Join user -> return Active(msgs, users |> Set.add user), box true | Active(msgs, users), Leave user -> let remaining = users |> Set.remove user if Set.isEmpty remaining then return Empty, box true else return Active(msgs, remaining), box true | Active(msgs, users), Send(user, text) -> let entry = $"[{DateTime.UtcNow:HH:mm}] {user}: {text}" return Active(entry :: msgs, users), box entry | _, GetHistory -> let msgs = match state with Active(m, _) -> m | _ -> [] return state, box msgs | _, GetParticipants -> let users = match state with Active(_, u) -> u | _ -> Set.empty return state, box users | _ -> return state, box false })
persist "Default" interleaveMessage typeof<ChatCommand> // read-heavy queries may interleave
onTimer "Cleanup" (TimeSpan.FromMinutes 5.) (TimeSpan.FromMinutes 5.) (fun state -> task { match state with | Active(msgs, users) when msgs.Length > 1000 -> return Active(msgs |> List.take 500, users) | _ -> return state }) }Next steps
Section titled “Next steps”- Silo Configuration — configure storage, clustering, and streaming for your grains
- Streaming — publish and subscribe to events
- Testing — test your grain definitions with FsCheck and TestHarness
- Advanced — transactions, grain directory, OpenTelemetry, shutdown, state migration