Skip to content

Neftedollar/orleans-fsharp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

234 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Orleans.FSharp

Orleans.FSharp logo

Idiomatic F# for Microsoft Orleans -- computation expressions, not boilerplate

CI License: MIT .NET 10 Orleans 10 F# Tests NuGet


Why this exists

Orleans is a powerful virtual actor framework, but using it from F# means fighting C# idioms at every turn: mutable state bags, attribute-heavy classes, verbose DI wiring. Orleans.FSharp replaces all of that with computation expressions that let you define grains, configure silos, and wire streaming in natural F# style -- discriminated unions as state, pure handler functions, and declarative configuration. The full Orleans runtime does the heavy lifting underneath.

Quick Start

open Orleans.FSharp
open Orleans.FSharp.Runtime

// 1. Define state and commands — plain F# types, no attributes needed
type CounterState = { Count: int }
type CounterCommand = Increment | Decrement | GetValue

// 2. Define the grain with a computation expression
let counter = grain {
    defaultState { Count = 0 }
    handle (fun state cmd -> task {
        match cmd with
        | Increment -> return { Count = state.Count + 1 }, box (state.Count + 1)
        | Decrement -> return { Count = state.Count - 1 }, box (state.Count - 1)
        | GetValue  -> return state, box state.Count
    })
    persist "Default"
}

// 3. Configure the silo — clean F#, no C# extension method chains
let config = siloConfig {
    useLocalhostClustering
    addMemoryStorage "Default"
    useJsonFallbackSerialization  // enables clean types without Orleans attributes
}

Feature Showcase

grain { } -- Grain Definition

Keyword Description
defaultState Set the initial state value
handle Register a state -> msg -> Task<state * obj> handler
handleState Simpler: state -> msg -> Task<state> — result IS the new state
handleTyped Typed result without manual boxing: state -> msg -> Task<state * 'R>
handleWithContext Handler with GrainContext for grain-to-grain calls and DI
handleStateWithContext GrainContext + state-only result
handleTypedWithContext GrainContext + typed result
handleWithServices Alias for handleWithContext emphasizing DI access
handleStateWithServices Services + state-only result
handleTypedWithServices Services + typed result
handleCancellable Handler with CancellationToken support
handleStateCancellable State-only result + cancellation
handleTypedCancellable Typed result + cancellation
handleWithContextCancellable Context + cancellation
handleWithServicesCancellable Services + cancellation
persist Name the storage provider for state persistence
additionalState Declare a named secondary persistent state
onActivate Hook that runs on grain activation
onDeactivate Hook that runs on grain deactivation
onReminder Register a named reminder handler
onTimer Register a declarative timer with dueTime + period
onLifecycleStage Hook into grain lifecycle stages
interleaveMessage Allow a message type to interleave: interleaveMessage typeof<Query>

Per-grain Orleans attributes — use the C# CodeGen path. [Reentrant], [StatelessWorker], [MayInterleave], [ReadOnly], [OneWay], placement strategies, [ImplicitStreamSubscription], and [GrainType] are applied through the per-grain Orleans.FSharp.CodeGen path, where each grain compiles to its own C# class/method that carries the real Orleans attribute. They are not grain { } CE keywords: the universal grain pattern shares a single FSharpGrainImpl class and one handler method, so per-grain class/method attributes cannot be expressed there. The one reentrancy lever that fits the universal pattern is interleaveMessage typeof<'Msg>.

siloConfig { } -- Silo Configuration

Keyword Description
useLocalhostClustering Local dev clustering
addRedisClustering Redis-based clustering
addAzureTableClustering Azure Table clustering
addAdoNetClustering ADO.NET clustering (Postgres, SQL Server)
addMemoryStorage In-memory grain storage
addRedisStorage Redis grain storage
addAzureBlobStorage Azure Blob grain storage
addAzureTableStorage Azure Table grain storage
addAdoNetStorage ADO.NET grain storage
addCosmosStorage Cosmos DB grain storage
addDynamoDbStorage DynamoDB grain storage
addCustomStorage Custom storage provider
addMemoryStreams In-memory stream provider
addPersistentStreams Durable stream provider
addBroadcastChannel Broadcast channel provider
addMemoryReminderService In-memory reminders
addRedisReminderService Redis reminders
addCustomReminderService Custom reminder service
useSerilog Wire Serilog as logging provider
configureServices Register custom DI services
addIncomingFilter Incoming grain call filter
addOutgoingFilter Outgoing grain call filter
addGrainService Register a GrainService type
addStartupTask Run a task when the silo starts
enableHealthChecks Register health check endpoints
useTls / useTlsWithCertificate TLS encryption
useMutualTls / useMutualTlsWithCertificate Mutual TLS
addDashboard / addDashboardWithOptions Orleans Dashboard
useGrainVersioning Grain interface versioning
clusterId / serviceId / siloName Cluster identity
siloPort / gatewayPort / advertisedIpAddress Endpoints
grainCollectionAge Global idle deactivation timeout

clientConfig { } -- Client Configuration

Keyword Description
useLocalhostClustering Local dev clustering
useStaticClustering Static gateway endpoints
addMemoryStreams In-memory stream provider
configureServices Register custom DI services
useTls / useTlsWithCertificate TLS encryption
useMutualTls Mutual TLS
clusterId / serviceId Cluster identity
gatewayListRefreshPeriod Gateway refresh interval
preferredGatewayIndex Preferred gateway

Universal Grain Pattern — zero C# stubs

Call any registered F# grain without defining a per-grain C# interface:

// Silo startup — register your grain definition
siloBuilder.Services.AddFSharpGrain<PingState, PingCommand>(pingGrain) |> ignore

// Client / handler — string, GUID, or int key
let handle = FSharpGrain.ref<PingState, PingCommand> factory "ping-1"
let! state  = handle |> FSharpGrain.send Ping          // returns Task<PingState>
do! handle  |> FSharpGrain.post Ping                   // true one-way: fire-and-forget, no round-trip

// ask returns a type you choose — useful when the handler returns something other than the state
let! count  = handle |> FSharpGrain.ask<PingState, PingCommand, int> GetCount

// GUID and integer keys
let h = FSharpGrain.refGuid<S, M> factory (Guid.NewGuid())
let! s = h |> FSharpGrain.sendGuid MyCommand
let! r = h |> FSharpGrain.askGuid<S, M, string> QueryCmd

let h = FSharpGrain.refInt<S, M> factory 42L
do! h |> FSharpGrain.postInt MyCommand

The universal pattern works with any F# discriminated union as the command type — including cases with fields (Append of string) and nullary cases in mixed DUs. No CodeGen project is required; Orleans discovers the grains through Orleans.FSharp.Abstractions.

eventSourcedGrain { } -- Event Sourcing

Keyword Description
defaultState Initial state before any events
apply Pure event fold: state -> event -> state
handle Command handler: state -> command -> event list
logConsistencyProvider Orleans log consistency provider name

Installation

dotnet add package Orleans.FSharp
dotnet add package Orleans.FSharp.Runtime

Silo-side proxy generation (required for Orleans to locate your grains):

dotnet add package Orleans.FSharp.Abstractions

Optional packages:

dotnet add package Orleans.FSharp.EventSourcing  # Event sourcing
dotnet add package Orleans.FSharp.Testing         # Test harness + FsCheck

Why Orleans.FSharp.Abstractions? Orleans source generators only run on C# projects. Abstractions is a tiny C# shim (no code to write) that lets the Orleans runtime generate the proxy classes for IFSharpGrain. Reference it from your silo project — that's it.

Project Template

Scaffold a new project in seconds:

dotnet new install Orleans.FSharp.Templates
dotnet new orleans-fsharp -n MyApp

Upgrading to 3.0

3.0 is a breaking major. The Universal Grain Pattern (AddFSharpGrain + FSharpGrain.ref/send/ask/post) is now the canonical path, and the non-functional grain { } CE keywords that were deprecated in 2.x have been removed: reentrant, statelessWorker, maxActivations, the old string-based mayInterleave, interleave, oneWay, readOnly, grainType, deactivationTimeout, implicitStreamSubscription, and the placement operations (preferLocalPlacement, randomPlacement, hashBasedPlacement, activationCountPlacement, resourceOptimizedPlacement, siloRolePlacement, customPlacement). To apply the equivalent Orleans attributes per grain, use the Orleans.FSharp.CodeGen path. To allow a message type to interleave under the universal pattern, use interleaveMessage typeof<'Msg>. FSharpGrain.post is now a true one-way (fire-and-forget) call. See the CHANGELOG for the full breaking-change list.

Documentation

Guide Description
Getting Started Zero to working grain in 15 minutes
Grain Definition Complete grain { } CE reference
Silo Configuration Complete siloConfig { } CE reference
Client Configuration clientConfig { } CE reference
Serialization 3 modes: F# Binary, JSON, Orleans Native
Streaming Publish, subscribe, TaskSeq, broadcast
Event Sourcing eventSourcedGrain { } CE guide
Testing TestHarness, FsCheck, GrainMock
Analyzers OF0001: async {} detection, AllowAsync opt-out
Security TLS, mTLS, filters, secrets
Advanced Transactions, OpenTelemetry, shutdown, migration
Resilience Polly v8 retry, circuit-breaker, and timeout patterns
Redis Example End-to-end shopping cart with Redis storage/clustering
API Reference All public modules, types, functions

Package Structure

Package Description
Orleans.FSharp Core: grain CE, GrainRef, streaming, logging, reminders, timers, observers, serialization
Orleans.FSharp.Runtime Silo hosting, client config, grain discovery
Orleans.FSharp.Abstractions C# shim — Orleans proxy generation for IFSharpGrain (reference from silo)
Orleans.FSharp.EventSourcing Event-sourced grain CE
Orleans.FSharp.CodeGen Optional: per-grain C# code generation for custom grain interfaces (legacy pattern)
Orleans.FSharp.Testing Test harness, GrainArbitrary, GrainMock, log capture
Orleans.FSharp.Analyzers F# analyzer: OF0001 warns on async { } usage; [<AllowAsync>] opt-out
Orleans.FSharp.Templates dotnet new project template

Security

Connection Strings

Never inline connection strings containing passwords or secrets in source code. Load them from configuration or environment variables at runtime.

Recommended: Use IConfiguration or environment variables:

let connStr = Environment.GetEnvironmentVariable("REDIS_CONNECTION")

let config = siloConfig {
    useLocalhostClustering
    addRedisStorage "Default" connStr
}

Avoid: Hardcoding secrets in source files:

// DO NOT do this -- secrets will leak into version control
addRedisStorage "Default" "redis://user:password@host:6379"

TLS Certificates

When using useTls or useMutualTls, always use valid certificates from a trusted certificate authority in production. Do not disable certificate validation in production environments.

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.

License

This project is licensed under the MIT License.

About

Idiomatic F# for Microsoft Orleans — define grains with computation expressions, not boilerplate. Grains, streaming, event sourcing, transactions. Orleans 10.2.1 parity, 1700+ tests.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

24 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages