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.JavaScriptTutorial: App Record
This tutorial starts where Explicit Dependencies First leaves off.
The problem is not that explicit parameters are wrong. The problem is repetition:
- every helper has to thread the same dependencies
- adding one more dependency means touching many signatures
- the execution boundary grows as the feature adds dependencies
An app record solves that by bundling dependencies once at the boundary while keeping the workflow code explicit.
1. Reuse The Same Interfaces
open System
open System.Threading.Tasks
type OrderId = OrderId of Guid
type Order =
{ Id: OrderId
Email: string
Total: decimal }
type PlaceOrderError =
| InvalidEmail
| OrderRejected of string
| AuditWriteFailed
type IOrderRepository =
abstract Save : Order -> Task<Result<unit, string>>
type IEmailSender =
abstract SendConfirmation : Order -> Task
type IAuditLog =
abstract Write : string -> Task<Result<unit, unit>>
SystemThreadingTasks05-dependencies_05-tutorials_02-app-record.md_page.OrderIdOrderIdSystem.GuidRepresents a globally unique identifier (GUID).
05-dependencies_05-tutorials_02-app-record.md_page.OrderId: OrderIdEmail: stringstringAn abbreviation for the CLI type . Basic Types
Total: decimaldecimalAn abbreviation for the CLI type . Basic Types
05-dependencies_05-tutorials_02-app-record.md_page.PlaceOrderErrorInvalidEmailOrderRejectedAuditWriteFailed05-dependencies_05-tutorials_02-app-record.md_page.IOrderRepositorySave: IOrderRepository -> Order -> Task<Result<unit,string>>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
05-dependencies_05-tutorials_02-app-record.md_page.IEmailSenderSendConfirmation: IEmailSender -> Order -> TaskSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
05-dependencies_05-tutorials_02-app-record.md_page.IAuditLogWrite: IAuditLog -> string -> Task<Result<unit,unit>>type AppEnv =
{ Orders: IOrderRepository
Email: IEmailSender
Audit: IAuditLog }
05-dependencies_05-tutorials_02-app-record.md_page.AppEnvOrders: IOrderRepository05-dependencies_05-tutorials_02-app-record.md_page.IOrderRepositoryEmail: IEmailSender05-dependencies_05-tutorials_02-app-record.md_page.IEmailSenderAudit: IAuditLog05-dependencies_05-tutorials_02-app-record.md_page.IAuditLog3. Compose Several Flows
let validateOrder (order: Order) : Result<Order, PlaceOrderError> =
if String.IsNullOrWhiteSpace order.Email then
Error InvalidEmail
else
Ok order
let saveOrder (order: Order) : Flow<AppEnv, PlaceOrderError, Order> =
flow {
let! orders = Flow.envWith _.Orders
do!
Flow.fromTaskResult(fun _ -> orders.Save order)
|> Flow.mapError OrderRejected
return order
}
let sendConfirmation (order: Order) : Flow<AppEnv, PlaceOrderError, unit> =
flow {
let! email = Flow.envWith _.Email
do!
ColdTask(fun _ ->
task {
do! email.SendConfirmation order
return ()
})
}
let writeAudit (message: string) : Flow<AppEnv, PlaceOrderError, unit> =
flow {
let! audit = Flow.envWith _.Audit
return!
Flow.fromTaskResult(fun _ -> audit.Write message)
|> Flow.mapError (fun () -> AuditWriteFailed)
}
let placeOrder (order: Order) : Flow<AppEnv, PlaceOrderError, OrderId> =
flow {
let! validOrder = validateOrder order
let! savedOrder = saveOrder validOrder
do! sendConfirmation savedOrder
do! writeAudit $"Placed {savedOrder.Email} for {savedOrder.Total}"
return savedOrder.Id
}
validateOrder: Order -> Result<Order,PlaceOrderError>order: Order05-dependencies_05-tutorials_02-app-record.md_page.OrderMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
05-dependencies_05-tutorials_02-app-record.md_page.PlaceOrderErrorSystem.StringRepresents text as a sequence of UTF-16 code units.
IsNullOrWhiteSpace: string -> boolIndicates whether a specified string is , empty, or consists only of white-space characters. The string to test. if the parameter is or , or if consists exclusively of white-space characters.
Email: stringErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
InvalidEmailOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
saveOrder: Order -> Flow<AppEnv,PlaceOrderError,Order>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
05-dependencies_05-tutorials_02-app-record.md_page.AppEnvflow: FlowBuilderThe universal flow { } computation expression.
orders: IOrderRepositoryAxial.FlowenvWith: ('env -> 'value) -> Flow<'env,'error,'value>Projects one value from the current environment. This is the primary way to access app dependencies, configuration, or request metadata stored in env. The projection runs only when the flow is executed, so constructing the flow is still pure and side-effect free. Prefer small projections over passing a large environment deeper into reusable helpers. A function that extracts a value from the environment. A containing the projected value. let currentTime () = Flow.envWith (fun (environment: BaseRuntime) -> environment.Clock.UtcNow())
_arg1: AppEnvOrders: IOrderRepositoryfromTaskResult: (CancellationToken -> Task<Result<'value,'error>>) -> Flow<'env,'error,'value>Creates a flow from a cold task factory whose Error enters the typed error channel. The factory runs on each execution and receives the runtime cancellation token. Thrown exceptions are defects. .NET only
Save: Order -> Task<Result<unit,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
mapError: ('error -> 'nextError) -> Flow<'env,'error,'value> -> Flow<'env,'nextError,'value>Maps the error value of a synchronous flow. Transforms the error type of the flow while leaving successful values untouched. Useful for mapping internal errors into public-facing domain errors. The function to transform the error value. The source flow. A with the transformed error type. let flow = Flow.fail "error" |> Flow.mapError (fun err -> err + "!")
OrderRejectedsendConfirmation: Order -> Flow<AppEnv,PlaceOrderError,unit>unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
email: IEmailSenderEmail: IEmailSenderColdTasktask: TaskBuilderBuilds a task using computation expression syntax.
SendConfirmation: Order -> TaskwriteAudit: string -> Flow<AppEnv,PlaceOrderError,unit>message: stringstringAn abbreviation for the CLI type . Basic Types
audit: IAuditLogAudit: IAuditLogWrite: string -> Task<Result<unit,unit>>AuditWriteFailedplaceOrder: Order -> Flow<AppEnv,PlaceOrderError,OrderId>05-dependencies_05-tutorials_02-app-record.md_page.OrderIdvalidOrder: OrdersavedOrder: OrderId: OrderId- helper functions stop carrying dependency parameters
- helper functions still say exactly which fields they read
- adding another dependency does not force you to redesign the whole feature
4. Real Implementations
type SqlOrderRepository() =
interface IOrderRepository with
member _.Save order =
task {
// Imagine the real dependency here: database transaction, ORM, etc.
return Ok ()
}
type SmtpEmailSender() =
interface IEmailSender with
member _.SendConfirmation order =
task {
// Imagine the real dependency here: SMTP or email API client.
}
type FileAuditLog() =
interface IAuditLog with
member _.Write message =
task {
// Imagine the real dependency here: file append, structured logger, queue, etc.
return Ok ()
}
05-dependencies_05-tutorials_02-app-record.md_page.SqlOrderRepository05-dependencies_05-tutorials_02-app-record.md_page.IOrderRepository_: SqlOrderRepositorySave: SqlOrderRepository -> Order -> Task<Result<unit,string>>order: Ordertask: TaskBuilderBuilds a task using computation expression syntax.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
05-dependencies_05-tutorials_02-app-record.md_page.SmtpEmailSender05-dependencies_05-tutorials_02-app-record.md_page.IEmailSender_: SmtpEmailSenderSendConfirmation: SmtpEmailSender -> Order -> Task05-dependencies_05-tutorials_02-app-record.md_page.FileAuditLog05-dependencies_05-tutorials_02-app-record.md_page.IAuditLog_: FileAuditLogWrite: FileAuditLog -> string -> Task<Result<unit,unit>>message: stringtype RecordingOrders(saved: ResizeArray<Order>) =
interface IOrderRepository with
member _.Save order =
task {
saved.Add order
return Ok ()
}
type RecordingEmails(sent: ResizeArray<string>) =
interface IEmailSender with
member _.SendConfirmation order =
task {
sent.Add order.Email
}
type RecordingAudit(entries: ResizeArray<string>) =
interface IAuditLog with
member _.Write message =
task {
entries.Add message
return Ok ()
}
05-dependencies_05-tutorials_02-app-record.md_page.RecordingOrderssaved: ResizeArray<Order>ResizeArrayAn abbreviation for the CLI type
05-dependencies_05-tutorials_02-app-record.md_page.Order05-dependencies_05-tutorials_02-app-record.md_page.IOrderRepository_: RecordingOrdersSave: RecordingOrders -> Order -> Task<Result<unit,string>>order: Ordertask: TaskBuilderBuilds a task using computation expression syntax.
Add: Order -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
05-dependencies_05-tutorials_02-app-record.md_page.RecordingEmailssent: ResizeArray<string>stringAn abbreviation for the CLI type . Basic Types
05-dependencies_05-tutorials_02-app-record.md_page.IEmailSender_: RecordingEmailsSendConfirmation: RecordingEmails -> Order -> TaskAdd: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
Email: string05-dependencies_05-tutorials_02-app-record.md_page.RecordingAuditentries: ResizeArray<string>05-dependencies_05-tutorials_02-app-record.md_page.IAuditLog_: RecordingAuditWrite: RecordingAudit -> string -> Task<Result<unit,unit>>message: stringlet run () = task {
let env =
{ Orders = SqlOrderRepository() :> IOrderRepository
Email = SmtpEmailSender() :> IEmailSender
Audit = FileAuditLog() :> IAuditLog }
let order =
{ Id = OrderId(Guid.NewGuid())
Email = "ada@example.com"
Total = 125m }
let! exit = placeOrder order |> Flow.startTask env
match exit with
| Exit.Success orderId ->
printfn "Placed %A" orderId
| Exit.Failure cause ->
printfn "%s" (Cause.prettyPrint (function
| InvalidEmail -> "invalid email"
| OrderRejected reason -> reason
| AuditWriteFailed -> "audit write failed") cause)
}
run: unit -> Task<unit>task: TaskBuilderBuilds a task using computation expression syntax.
env: AppEnvOrders: IOrderRepository``.ctor``: unit -> SqlOrderRepository05-dependencies_05-tutorials_02-app-record.md_page.IOrderRepositoryEmail: IEmailSender``.ctor``: unit -> SmtpEmailSender05-dependencies_05-tutorials_02-app-record.md_page.IEmailSenderAudit: IAuditLog``.ctor``: unit -> FileAuditLog05-dependencies_05-tutorials_02-app-record.md_page.IAuditLogorder: OrderId: OrderIdOrderIdSystem.GuidRepresents a globally unique identifier (GUID).
NewGuid: unit -> GuidInitializes a new instance of the structure. A new GUID object.
Email: stringTotal: decimalexit: Exit<OrderId,PlaceOrderError>placeOrder: Order -> Flow<AppEnv,PlaceOrderError,OrderId>(|>): '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
Axial.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.
orderId: OrderIdprintfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
FailureThe workflow failed due to a specific cause.
cause: Cause<PlaceOrderError>Axial.CauseprettyPrint: ('error -> string) -> Cause<'error> -> stringPretty prints a cause tree for diagnostics.
InvalidEmailOrderRejectedreason: stringAuditWriteFailedAn app record is enough for a lot of applications.
Move beyond it when:
- you want reusable helpers to depend on named contracts instead of record field names
- you want startup-time provisioning with failure handling
- you want scope-owned resources and cleanup
Continue with Tutorial: Composing Built-in Services to add BaseRuntime
alongside dependencies like this one, Tutorial: Creating Reusable Services to
name a contract like IOrderRepository the way the built-in services are named, and then
Tutorial: Layers.

