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.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptTutorial: Composing Built-in Services
An application can combine services constrained by IHasX interfaces with its own dependencies in one environment.
The problem
BaseRuntime bundles the five platform services and already implements
IHasClock, IHasLog, IHasRandom, IHasGuid, and IHasEnvironmentVariables. Embedding it as a field of your
own record does not carry those interface implementations with it: F# has no mechanism for one type to forward
another type's interfaces automatically. Your own environment record has to state, once per service, where that
service lives:
Show page setup
// Setup for the checked examples on this page.
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.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen Axial.PlatformService
type AppEnv =
{ Runtime: BaseRuntime }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasLog with
member this.Log = this.Runtime.Log
interface IHasEnvironmentVariables with
member this.EnvironmentVariables = this.Runtime.EnvironmentVariables
AxialPlatformServiceFsLiveDocsGeneratedPage5_F9357565CFEE.AppEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Axial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: AppEnvClock: AppEnv -> unit -> IClockClock: IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: AppEnv -> unit -> ILogLog: ILogAxial.PlatformService.IHasEnvironmentVariablesDeclares that an environment supplies the environment-variable service.
EnvironmentVariables: AppEnv -> unit -> IEnvironmentVariablesEnvironmentVariables: IEnvironmentVariablesmember this.Clock = this.Runtime.Clock delegates the contract to the Runtime field. Declare interface members for the services the application uses, once on the environment record.
Use the services
The workflow uses the same service APIs whether Runtime holds them or they are top-level fields:
let loadMode : Flow<AppEnv, EnvironmentVariableError, string> =
flow {
let! now = Clock.utcDateTime
let! mode = EnvironmentVariable.get "APP_MODE"
do! Log.info $"[{now:O}] starting in mode {mode}"
return mode
}
loadMode: Flow<AppEnv,EnvironmentVariableError,string>Axial.Flow`3FsLiveDocsGeneratedPage5_F9357565CFEE.AppEnvAxial.PlatformService.EnvironmentVariableErrorstringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
now: DateTimeAxial.PlatformService.ClockHelpers for the clock service.
utcDateTime: Flow<'env,'error,DateTime>Reads the current UTC date/time from an explicit clock service.
mode: stringAxial.PlatformService.EnvironmentVariableHelpers for reading and parsing environment variables through an explicit service.
get: string -> Flow<'env,EnvironmentVariableError,string>Reads a raw string environment variable through an explicit service.
Axial.PlatformService.LogHelpers for the logging service.
info: string -> Flow<'env,'error,unit>Writes an informational log message through an explicit logging service.
loadMode does not know that Clock, Log, and EnvironmentVariable all come from the same Runtime field; it
only knows the three interfaces. Swap AppEnv for any other type that implements them and the
workflow is unchanged.
Add your own dependencies alongside it
AppEnv is an ordinary record, so extending it with an application-specific dependency is the same pattern as
the app record tutorial: add a field, add an interface if other
helpers should depend on the contract rather than the field name directly:
type IOrderRepository =
abstract Count: unit -> int
type ShopEnv =
{ Runtime: BaseRuntime
Orders: IOrderRepository }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasLog with
member this.Log = this.Runtime.Log
interface IHasEnvironmentVariables with
member this.EnvironmentVariables = this.Runtime.EnvironmentVariables
FsLiveDocsGeneratedPage5_F9357565CFEE.IOrderRepositoryCount: IOrderRepository -> unit -> intunitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
intAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage5_F9357565CFEE.ShopEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Orders: IOrderRepositoryAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: ShopEnvClock: ShopEnv -> unit -> IClockClock: IClockAxial.PlatformService.IHasLogDeclares that an environment supplies the logging service.
Log: ShopEnv -> unit -> ILogLog: ILogAxial.PlatformService.IHasEnvironmentVariablesDeclares that an environment supplies the environment-variable service.
EnvironmentVariables: ShopEnv -> unit -> IEnvironmentVariablesEnvironmentVariables: IEnvironmentVariablesA workflow that reads Orders directly (Flow.envWith _.Orders) is coupled to this record's field name. If you
want Orders reusable behind a named contract instead, the way Clock and Log are, see
Tutorial: Creating Reusable Services.
Run it
In production the environment holds the live services:
let runLive () : Task<Exit<string, EnvironmentVariableError>> =
loadMode |> Flow.startTask { Runtime = BaseRuntime.liveValue }
runLive: unit -> Task<Exit<string,EnvironmentVariableError>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
stringAn abbreviation for the CLI type . Basic Types
Axial.PlatformService.EnvironmentVariableErrorloadMode: Flow<AppEnv,EnvironmentVariableError,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.FlowstartTask: 'env -> Flow<'env,'error,'value> -> Task<Exit<'value,'error>>Starts the workflow immediately and returns a task handle for its final exit. The work is already in flight when this returns. Use Flow.toAsync for a cold handle. The environment used by the workflow. The workflow to start. A task that completes with the workflow exit. let running = workflow |> Flow.startTask environment
Runtime: BaseRuntimeAxial.PlatformService.BaseRuntimeModuleHelpers for constructing the standard explicit service bundle used by workflow hosts.
liveValue: BaseRuntimeCreates the standard live base runtime as an explicit service bundle.
Test it
Tests can substitute services through BaseRuntime's fields. BaseRuntime's fields each accept the fixed value shown in
deterministic implementations, so a test builds the
whole environment as one value literal, with no interface to reimplement:
let gate = obj()
let mutable logged = []
let testEnv =
{ Runtime =
{ Clock = Clock.fromValue (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
Log = Log.fromSink (fun _ message -> lock gate (fun () -> logged <- message :: logged))
Random = Random.fromValue 7
Guid = Guid.fromValue (Guid.Parse "00000000-0000-0000-0000-000000000001")
EnvironmentVariables = EnvironmentVariables.fromPairs [ "APP_MODE", "diagnostic" ] } }
loadMode |> Flow.run testEnv |> shouldEqual (Exit.Success "diagnostic")
lock gate (fun () -> List.rev logged) |> shouldEqual [ "[2026-01-01T00:00:00.0000000Z] starting in mode diagnostic" ]
gate: obj``.ctor``: unit -> unitInitializes a new instance of the class.
logged: string listtestEnv: AppEnvRuntime: BaseRuntimeClock: IClockAxial.PlatformService.ClockHelpers for the clock service.
fromValue: DateTimeOffset -> IClockCreates a deterministic clock that always returns the supplied instant; measured durations are zero.
``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
Log: ILogAxial.PlatformService.LogHelpers for the logging service.
fromSink: (LogLevel -> string -> unit) -> ILogCreates a logger from a synchronous sink function. Exceptions are appended to the message text.
message: stringlock: 'Lock -> (unit -> 'T) -> 'TExecute the function as a mutual-exclusion region using the input value as a lock. The object to be locked. The action to perform during the lock. The resulting value. open System.Linq /// A counter object, supporting unlocked and locked increment type TestCounter () = let mutable count = 0 /// Increment the counter, unlocked member this.IncrementWithoutLock() = count <- count + 1 /// Increment the counter, locked member this.IncrementWithLock() = lock this (fun () -> count <- count + 1) /// Get the count member this.Count = count let counter = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter.IncrementWithoutLock()) // Evaluates to a number between 1-100000, non-deterministically because there is no locking counter.Count let counter2 = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter2.IncrementWithLock()) // Evaluates to 100000 deterministically because the increment to the counter object is locked counter2.Count
(::)Random: IRandomAxial.PlatformService.RandomHelpers for the random-number service.
fromValue: int -> IRandomCreates a deterministic random generator that always returns the supplied value. NextDouble returns the value as a float and NextBytes truncates it to a byte; use Random.fromFixed when a test needs distinct integer, double, and byte values.
Guid: IGuidAxial.PlatformService.GuidHelpers for the GUID service.
fromValue: Guid -> IGuidCreates a deterministic GUID service that always returns the supplied value.
System.GuidRepresents a globally unique identifier (GUID).
Parse: string -> GuidConverts the string representation of a GUID to the equivalent structure. The string to convert. A structure that contains the value that was parsed. is . is not in a recognized format.
EnvironmentVariables: IEnvironmentVariablesAxial.PlatformService.EnvironmentVariablesHelpers for the environment-variable service.
fromPairs: (string * string) seq -> IEnvironmentVariablesCreates a deterministic provider from a fixed set of name/value pairs.
loadMode: Flow<AppEnv,EnvironmentVariableError,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
rev: 'T list -> 'T listReturns a new list with the elements in reverse order. The input list. The reversed list. let inputs = [ 0; 1; 2 ] inputs |> List.rev Evaluates to [ 2; 1; 0 ]. This is an O(n) operation, where n is the length of the list.
See Tutorial: Creating Reusable Services for an application-specific service contract, or Layers when building the environment itself becomes effectful.

