Skip to content
Orleans.FSharp

Legacy: Redis Example — Shopping Cart Service

Section titled “Legacy: Redis Example — Shopping Cart Service”

Archived and unsupported. This material is retained only to help migrate existing applications. There is no new Legacy release line, feature or compatibility work, or security fixes. New development must use the current functional API.

End-to-end guide — Redis storage, clustering, and reminders for a real-world shopping cart.

Note. This walkthrough is written against the grain { } CE, AddFSharpGrain and the FSharpGrain.ref/send/post/ask handle module from the 4.1-and-earlier package shape. The archived project is not verified against 5.0. For the current grain authoring model see functional-grains.md.

A shopping cart service where each cart is a durable Orleans grain backed by Redis. The service uses:

  • Redis clustering — silos discover each other through Redis instead of Kubernetes or Azure
  • Redis grain storage — cart state survives silo restarts
  • Redis reminders — an idle-cart cleanup reminder fires after 30 minutes of inactivity
  • Universal grain pattern — no C# interface stubs needed

Terminal window
docker run -d -p 6379:6379 redis:7-alpine
Terminal window
dotnet add package Orleans.FSharp
dotnet add package Orleans.FSharp.Runtime
dotnet add package Orleans.FSharp.Abstractions # C# shim for Orleans proxy gen
# Redis providers — optional at reference time, required at runtime
dotnet add package Microsoft.Orleans.Clustering.Redis
dotnet add package Microsoft.Orleans.Persistence.Redis
dotnet add package Microsoft.Orleans.Reminders.Redis

open Orleans.FSharp
type CartItem = { ProductId: string; Quantity: int; UnitPrice: decimal }
type CartState =
{ Items: CartItem list
CheckedOut: bool }
type CartCommand =
| AddItem of productId: string * qty: int * unitPrice: decimal
| RemoveItem of productId: string
| GetItems
| Checkout
| Clear

open Orleans.FSharp
open Orleans.FSharp.Runtime
let cartGrain =
grain {
defaultState { Items = []; CheckedOut = false }
persist "Default" // maps to the "Default" Redis storage provider
// Activate hook: log the cart creation
onActivate (fun state ->
task {
printfn "Cart grain activated"
return state
})
// Idle-cart cleanup: remind after 30 minutes of inactivity
onReminder "idle-cleanup" (fun state reminderName tickStatus ->
task {
// Deactivate empty or checked-out carts after inactivity
if state.Items.IsEmpty || state.CheckedOut then
printfn "Reminder '%s' fired — cart is empty or checked out, will deactivate" reminderName
return state
})
handle (fun state cmd -> task {
match cmd with
| AddItem(productId, qty, price) ->
if state.CheckedOut then
return state, box state // ignore — cart is closed
else
let existing = state.Items |> List.tryFind (fun i -> i.ProductId = productId)
let updated =
match existing with
| Some item ->
state.Items
|> List.map (fun i ->
if i.ProductId = productId
then { i with Quantity = i.Quantity + qty }
else i)
| None ->
{ ProductId = productId; Quantity = qty; UnitPrice = price }
:: state.Items
let newState = { state with Items = updated }
return newState, box newState
| RemoveItem productId ->
let newState =
{ state with Items = state.Items |> List.filter (fun i -> i.ProductId <> productId) }
return newState, box newState
| GetItems ->
return state, box state.Items
| Checkout ->
let newState = { state with CheckedOut = true }
return newState, box newState
| Clear ->
let newState = { Items = []; CheckedOut = false }
return newState, box newState
})
}

open System
open Orleans.FSharp.Runtime
let redisConn = Environment.GetEnvironmentVariable("REDIS_CONNECTION") // "localhost:6379"
let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

The three Redis CE operations map to these Orleans packages:

CE operation NuGet package
addRedisClustering Microsoft.Orleans.Clustering.Redis
addRedisStorage Microsoft.Orleans.Persistence.Redis
addRedisReminderService Microsoft.Orleans.Reminders.Redis

open System
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Orleans.FSharp
open Orleans.FSharp.Runtime
[<EntryPoint>]
let main _ =
let redisConn =
Environment.GetEnvironmentVariable("REDIS_CONNECTION")
|> Option.ofObj
|> Option.defaultValue "localhost:6379"
let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
let builder = Host.CreateApplicationBuilder()
SiloConfig.applyToHost config builder
// Register the grain definition
builder.Services.AddFSharpGrain<CartState, CartCommand>(cartGrain) |> ignore
let host = builder.Build()
host.Run()
0

A standalone client (separate process or service) connects to the cluster via static gateway clustering. The gateway port matches the silo’s default port (30000).

open System
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Orleans.FSharp
open Orleans.FSharp.Runtime
let clientCfg = clientConfig {
useStaticClustering ["127.0.0.1:30000"]
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
let builder = HostApplicationBuilder()
ClientConfig.applyToHost clientCfg builder
let host = builder.Build()
host.Start()
let factory = host.Services.GetRequiredService<Orleans.IGrainFactory>()

For multi-silo production clusters, list multiple gateway addresses:

let clientCfg = clientConfig {
useStaticClustering ["10.0.0.1:30000"; "10.0.0.2:30000"; "10.0.0.3:30000"]
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

open Orleans.FSharp
// Get a handle for user "user-42"'s cart
let cart = FSharpGrain.ref<CartState, CartCommand> factory "user-42"
// Add items
let! _ = cart |> FSharpGrain.send (AddItem("sku-001", 2, 9.99m))
let! _ = cart |> FSharpGrain.send (AddItem("sku-002", 1, 24.50m))
// Read the items (ask — result is CartItem list, not full CartState)
let! items = cart |> FSharpGrain.ask<CartState, CartCommand, CartItem list> GetItems
printfn "Cart has %d line(s)" items.Length
// Remove an item
do! cart |> FSharpGrain.post (RemoveItem "sku-001")
// Checkout
let! finalState = cart |> FSharpGrain.send Checkout
printfn "Checked out: %b" finalState.CheckedOut
Function When to use
FSharpGrain.ref factory key Get a string-keyed handle
FSharpGrain.send cmd handle Send a command, get back the new state
FSharpGrain.post cmd handle Send a command, discard the result
FSharpGrain.ask<S,C,R> cmd handle Send a command, get back a specific result type (not the state)

Load connection strings from the environment

Section titled “Load connection strings from the environment”

Never inline connection strings in source code:

let redisConn =
Environment.GetEnvironmentVariable("REDIS_CONNECTION")
|> Option.ofObj
|> Option.defaultWith (fun () -> failwith "REDIS_CONNECTION is not set")

Or use IConfiguration:

open Microsoft.Extensions.Configuration
let redisConn = builder.Configuration.GetConnectionString("Redis")

Enable TLS between silos and clients using the useTls CE operation:

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
useTls "my-cert-subject"
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

For Redis itself, use a rediss:// connection string or pass TLS options through the underlying provider’s configuration.

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "Default" redisConn
addRedisReminderService redisConn
enableHealthChecks
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}

enableHealthChecks wires the Orleans liveness probe into ASP.NET Core’s /healthz endpoint (or your configured health check path).

Use a dedicated provider for frequently-written state to avoid sharing throughput:

let config = siloConfig {
addRedisClustering redisConn
addRedisStorage "CartStorage" redisConn // carts
addRedisStorage "SessionStore" redisConn // sessions on a different logical DB
addRedisReminderService redisConn
clusterId "shopping-cart-cluster"
serviceId "shopping-cart"
}
// In the grain:
let cartGrain = grain {
persist "CartStorage"
// ...
}