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: Creating Reusable Services
The built-in services all follow one shape: a narrow interface (IClock, IFileSystem), an IHasX marker the
environment implements, and a module of helpers constrained by that marker rather than by any particular field
name. Nothing about that shape is special to the library — it is an ordinary pattern, and this tutorial builds one
for a service Axial does not ship: a currency conversion rate.
Reach for it when several workflows should depend on the same named contract without being tied to one concrete
app record field name — the same reason built-in services are declared as IHasX instead
of read from a fixed field. A dependency used by exactly one workflow usually does not need this; see
choosing an approach.
Define the contract
open System.Threading.Tasks
type IExchangeRates =
abstract GetUsdToAud : unit -> Task<decimal>
SystemThreadingTasks14-advanced_03-custom-services.md_page.IExchangeRatesGetUsdToAud: IExchangeRates -> unit -> Task<decimal>unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
decimalAn abbreviation for the CLI type . Basic Types
Write a reusable helper
type IHasExchangeRates =
abstract ExchangeRates : IExchangeRates
[<RequireQualifiedAccess>]
module ExchangeRates =
let service<'env, 'error when 'env :> IHasExchangeRates> : Flow<'env, 'error, IExchangeRates> =
Flow.envWith _.ExchangeRates
let priceInAud<'env, 'error when 'env :> IHasExchangeRates>
(usdAmount: decimal)
: Flow<'env, 'error, decimal> =
flow {
let! rates = ExchangeRates.service
let! rate = ColdTask(fun _ -> rates.GetUsdToAud())
return usdAmount * rate
}
14-advanced_03-custom-services.md_page.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRates14-advanced_03-custom-services.md_page.IExchangeRatesMicrosoft.FSharp.Core.RequireQualifiedAccessAttributeThis attribute is used to indicate that references to the elements of a module, record or union type require explicit qualified access. Attributes
14-advanced_03-custom-services.md_page.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>enverrorAxial.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.
Axial.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: 'envExchangeRates: IExchangeRatespriceInAud: decimal -> Flow<'env,'error,decimal>usdAmount: decimaldecimalAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
rates: IExchangeRatesrate: decimalColdTaskGetUsdToAud: unit -> Task<decimal>(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
Give it a typed failure
priceInAud above lets a failed lookup surface as an unhandled Task exception, which is a defect, not something
a caller can react to. Most services worth naming this way are worth failing this way too — compare
FileSystemError or HttpError:
type ExchangeRateError =
| RateUnavailable of pair: string
| ProviderTimedOut
let priceInAud<'env>
(usdAmount: decimal)
: Flow<'env, ExchangeRateError, decimal> when 'env :> IHasExchangeRates =
flow {
let! rates = ExchangeRates.service
let! rate =
Flow.attemptTask (fun _ -> rates.GetUsdToAud())
|> Flow.mapError (fun _ -> ProviderTimedOut)
return usdAmount * rate
}Provide an app environment
type AppEnv =
{ Rates: IExchangeRates
Region: string }
interface IHasExchangeRates with
member this.ExchangeRates = this.Rates
14-advanced_03-custom-services.md_page.AppEnvRates: IExchangeRates14-advanced_03-custom-services.md_page.IExchangeRatesRegion: stringstringAn abbreviation for the CLI type . Basic Types
14-advanced_03-custom-services.md_page.IHasExchangeRatesthis: AppEnvExchangeRates: AppEnv -> unit -> IExchangeRatesA custom service composes into the same environment as BaseRuntime exactly the way two built-in services do —
each gets its own interface member, delegating to wherever the value actually lives. See
Tutorial: Composing Built-in Services for the BaseRuntime half of this:
open Axial.PlatformService
type AppEnv =
{ Runtime: BaseRuntime
Rates: IExchangeRates }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasExchangeRates with
member this.ExchangeRates = this.Ratestype FixedRates(rate: decimal) =
interface IExchangeRates with
member _.GetUsdToAud() = Task.FromResult rate
14-advanced_03-custom-services.md_page.FixedRatesrate: decimaldecimalAn abbreviation for the CLI type . Basic Types
14-advanced_03-custom-services.md_page.IExchangeRates_: FixedRatesGetUsdToAud: FixedRates -> unit -> Task<decimal>System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
Publish it from a package
Everything on this page lives in the application. When the contract, the helper module, and a live
implementation should ship to callers you will never see — the same relationship Axial.FileSystem has to
Axial.Core — see providing services from a package for the composable shape that
requires.
This is the main step from "an app record for one workflow" to "reusable helpers shared across workflows."

