Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

Tutorial: Layers

Layers are for construction time, not business logic time.

Use a layer when you need to:

  • build an environment from other services or config
  • fail during provisioning before the workflow starts
  • own resources that must be cleaned up exactly once
  • compose several independent startup steps in parallel

1. The Workflow Still Targets An Environment

open System
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers

type IOrders =
    abstract Save : string -> Task<unit>

type IClock =
    abstract UtcNow : unit -> DateTimeOffset

type AppEnv =
    { Orders: IOrders
      Clock: IClock }

let saveOrder (orderId: string) : Flow<AppEnv, string, unit> =
    flow {
        let! env = Flow.env
        do!
            ColdTask(fun _ ->
                task {
                    do! env.Orders.Save orderId
                    return ()
                })
        let now = env.Clock.UtcNow()
        printfn "[%O] saved %s" now orderId
    }
The workflow still depends on `AppEnv`. Layers only change how `AppEnv` gets built.

2. Build Small Layers

let ordersLayer : Layer<unit, string, IOrders> =
    Layer.succeed
        { new IOrders with
            member _.Save orderId =
                task {
                    // Imagine the real dependency here: open connection, transaction, etc.
                    printfn "persisting %s" orderId
                } }

let clockLayer : Layer<unit, string, IClock> =
    Layer.succeed
        { new IClock with
            member _.UtcNow() = DateTimeOffset.UtcNow }
## 3. Merge Them Into An App Layer
let appLayer : Layer<unit, string, AppEnv> =
    layer {
        let! orders = ordersLayer
        and! clock = clockLayer

        return
            { Orders = orders
              Clock = clock }
    }
Use plain `let!` when one provisioning step depends on another. Use sibling `and!` when the steps are independent.

4. Provision Failure Happens Before Business Logic

let failingOrdersLayer : Layer<unit, string, IOrders> =
    Layer.fromTask (fun _ _ ->
        task {
            return Exit.Failure (Cause.Fail "database connection string missing")
        })
If provisioning fails, `Layer.provide` never runs the downstream business workflow. That separation is one of the main reasons to use layers.

5. Resource Ownership

type FakeConnection() =
    interface IAsyncDisposable with
        member _.DisposeAsync() =
            ValueTask(Task.CompletedTask)

let connectionLayer : Layer<unit, string, FakeConnection> =
    Layer.acquireRelease
        (Layer.succeed (new FakeConnection()))
        (fun connection _ct -> connection.DisposeAsync().AsTask())
This is another main reason to use layers: acquired resources belong to the provisioning scope and are released when the provided workflow completes, fails, or is interrupted.

6. Run Through Layer.provide

let run () = task {
    let! exit =
        saveOrder "A-100"
        |> Layer.provide appLayer
        |> Flow.startTask ()

    match exit with
    | Exit.Success () -> printfn "done"
    | Exit.Failure cause -> printfn "failed: %A" cause
}
The call site stays small:
  • construct or choose the layer
  • provide it once
  • run the workflow

That is much cleaner than manually opening and closing startup resources around every feature entry point.