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
- Placement strategies, reentrancy, and stateless workers
- Multiple named states, implicit stream subscriptions, and more
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”reentrant
Section titled “reentrant”Marks the grain as reentrant, allowing concurrent message processing. By default, Orleans grains process one message at a time.
grain { defaultState Map.empty handle myReadHeavyHandler reentrant}interleave
Section titled “interleave”Marks a specific method as always interleaved, even on non-reentrant grains. Use for methods that are safe to run concurrently.
grain { defaultState { Data = Map.empty } handle myHandler interleave "GetStatus" interleave "GetVersion"}readOnly
Section titled “readOnly”Marks a method as read-only. Read-only methods are interleaved for concurrent reads on non-reentrant grains.
grain { defaultState { Items = [] } handle myHandler readOnly "GetItems" readOnly "GetCount"}mayInterleave
Section titled “mayInterleave”Sets a custom predicate method for reentrancy decisions. The named static method receives an InvokeMethodRequest and returns bool.
grain { defaultState Map.empty handle myHandler mayInterleave "ShouldInterleave"}Stateless Workers
Section titled “Stateless Workers”statelessWorker
Section titled “statelessWorker”Marks the grain as a stateless worker, allowing multiple activations per silo for load balancing. Stateless workers cannot use persistent state.
grain { defaultState () handle (fun _ msg -> task { let result = processMessage msg return (), box result }) statelessWorker}maxActivations
Section titled “maxActivations”Sets the maximum number of local worker activations per silo. Defaults to the number of CPU cores.
grain { defaultState () handle myHandler statelessWorker maxActivations 4}Placement Strategies
Section titled “Placement Strategies”Control which silo activates a grain:
// Prefer the silo where the call originatedgrain { ... ; preferLocalPlacement }
// Random silograin { ... ; randomPlacement }
// Consistent hash of grain IDgrain { ... ; hashBasedPlacement }
// Silo with fewest activationsgrain { ... ; activationCountPlacement }
// Resource-aware (CPU, memory)grain { ... ; resourceOptimizedPlacement }
// Target silos with a specific rolegrain { ... ; siloRolePlacement "worker" }
// Custom strategy typegrain { ... ; customPlacement typeof<MyPlacementStrategy> }Streaming
Section titled “Streaming”implicitStreamSubscription
Section titled “implicitStreamSubscription”Auto-subscribes the grain to a stream namespace. The handler receives the current state and a stream event (boxed), and returns the new state.
grain { defaultState { Events = [] } handle myHandler persist "Default"
implicitStreamSubscription "OrderEvents" (fun state event -> task { let orderEvent = event :?> OrderEvent return { state with Events = orderEvent :: state.Events } })}Method Annotations
Section titled “Method Annotations”oneWay
Section titled “oneWay”Marks a method as fire-and-forget. The caller does not wait for the grain to finish processing.
grain { defaultState () handle myHandler oneWay "LogEvent"}grainType
Section titled “grainType”Sets a custom grain type name (maps to [GrainType("name")] in CodeGen).
grain { defaultState 0 handle myHandler grainType "my-counter-v2"}deactivationTimeout
Section titled “deactivationTimeout”Sets the per-grain idle timeout before deactivation.
grain { defaultState Map.empty handle myHandler deactivationTimeout (TimeSpan.FromMinutes 30.)}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" reentrant readOnly "GetHistory" readOnly "GetParticipants" deactivationTimeout (TimeSpan.FromHours 1.)
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 — behavior pattern: state machines with
Behavior.run/Behavior.runWithContext