Skip to content
Orleans.FSharp

Pick the delivery mechanism from the relationship between caller and consumer.

Need Mechanism
One request and one reply API field returning Task<'Reply>
Read-only request readOnly + handleQuery
Fire-and-forget notification oneWay
Push to one connected client object Functional observer
Decoupled provider-backed pub/sub Orleans stream or broadcast channel
Items produced by one call API field returning IAsyncEnumerable<'Item>

A normal handler returns state and reply; a query returns only the reply:

grainFor CounterApi.contract {
defaultState (fun () -> 0)
handle (_.increment) (fun _ state () ->
task {
let next = state + 1
return next, next
})
handleQuery (_.value) (fun _ state () -> task { return state })
}

The contract must mark a handleQuery operation readOnly. This makes the no-state-write intent explicit in both the API and definition.

Orleans serializes turns per activation by default. Use reentrant for the whole contract or mayInterleave / alwaysInterleave for specific requests. These policies can expose intermediate state between awaits, so add them only around operations designed for it.

Do not add locks or Task.Run inside a grain. They conflict with Orleans scheduling rather than making a functional handler safer.

  • Functional observers are best-effort live push and must be subscribed again after a client or activation lifetime ends.
  • Orleans streams and broadcast channels are pub/sub abstractions selected and configured by a provider.
  • Server-streaming replies attach one producer to one call and propagate enumerator disposal back to that producer.

They compose, but they are not interchangeable durability guarantees.