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: 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. The library gives that shape no special treatment, so this tutorial builds one
for a service Axial does not ship: a currency conversion rate.
Use it when several workflows should depend on the same named contract without being tied to one concrete
app record field name, for 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
Shared 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 actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
open System.Threading.Tasks
type IExchangeRates =
abstract GetUsdToAud : unit -> Task<decimal>
SystemThreadingTasksFsLiveDocsGeneratedPage9_7555C511A9CE.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
Make it as narrow as the built-in ones. IExchangeRates exposes one conversion, not a general-purpose pricing
client; a workflow that needs more asks for more, the same way IHasClock does not also expose scheduling.
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
}
FsLiveDocsGeneratedPage9_7555C511A9CE.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRatesFsLiveDocsGeneratedPage9_7555C511A9CE.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
FsLiveDocsGeneratedPage9_7555C511A9CE.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
This helper no longer cares whether the caller stores the service in Rates, Runtime.ExchangeRates, or any other
field. It only needs IHasExchangeRates, the same generic-constraint pattern Clock.now and
FileSystem.readAllText use.
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 priceInAudOrFail<'env when 'env :> IHasExchangeRates>
(usdAmount: decimal)
: Flow<'env, ExchangeRateError, decimal> =
flow {
let! rates = ExchangeRates.service
let! rate =
Flow.attemptTask (fun _ -> rates.GetUsdToAud())
|> Flow.mapError (fun _ -> ProviderTimedOut)
return usdAmount * rate
}
FsLiveDocsGeneratedPage9_7555C511A9CE.ExchangeRateErrorRateUnavailablepair: stringstringAn abbreviation for the CLI type . Basic Types
ProviderTimedOutpriceInAudOrFail: decimal -> Flow<'env,ExchangeRateError,decimal>envFsLiveDocsGeneratedPage9_7555C511A9CE.IHasExchangeRatesusdAmount: decimaldecimalAn abbreviation for the CLI type . Basic Types
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.
flow: FlowBuilderThe universal flow { } computation expression.
rates: IExchangeRatesFsLiveDocsGeneratedPage9_7555C511A9CE.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>rate: decimalAxial.FlowattemptTask: (CancellationToken -> Task<'value>) -> Flow<'env,exn,'value>Creates a flow from a cancellable task factory and treats thrown exceptions as recoverable typed errors. Successful completion returns Exit.Success. OperationCanceledException returns Cause.Interrupt when the runtime token requested it. Other exceptions, including cancellation the operation raised for its own reasons, return Cause.Fail exn. Starts the operation, observing the supplied cancellation token. .NET only
GetUsdToAud: unit -> Task<decimal>(|>): '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 + "!")
(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
Now a caller can match RateUnavailable or ProviderTimedOut as ordinary values instead of catching an exception.
Provide an app environment
type AppEnv =
{ Rates: IExchangeRates
Region: string }
interface IHasExchangeRates with
member this.ExchangeRates = this.Rates
FsLiveDocsGeneratedPage9_7555C511A9CE.AppEnvRates: IExchangeRatesFsLiveDocsGeneratedPage9_7555C511A9CE.IExchangeRatesRegion: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage9_7555C511A9CE.IHasExchangeRatesthis: AppEnvExchangeRates: AppEnv -> unit -> IExchangeRateslet fixedRate = { new IExchangeRates with member _.GetUsdToAud() = Task.FromResult 1.5m }
let failing = { new IExchangeRates with member _.GetUsdToAud() = Task.FromException<decimal>(TimeoutException()) }
priceInAud 10m |> Flow.run { Rates = fixedRate; Region = "au" } |> shouldEqual (Exit.Success 15.0m : Exit<decimal, string>)
priceInAudOrFail 10m |> Flow.run { Rates = failing; Region = "au" } |> shouldEqual (Exit.Failure(Cause.Fail ProviderTimedOut))
fixedRate: IExchangeRatesFsLiveDocsGeneratedPage9_7555C511A9CE.IExchangeRates_: IExchangeRatesGetUsdToAud: 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.
failing: IExchangeRatesFromException: exn -> TaskCreates a that has completed with a specified exception. The exception with which to complete the task. The faulted task.
decimalAn abbreviation for the CLI type . Basic Types
``.ctor``: unit -> unitInitializes a new instance of the class.
priceInAud: decimal -> Flow<'env,'error,decimal>(|>): '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
Rates: IExchangeRatesRegion: stringshouldEqual: '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.
stringAn abbreviation for the CLI type . Basic Types
priceInAudOrFail: decimal -> Flow<'env,ExchangeRateError,decimal>FailureThe workflow failed due to a specific cause.
Axial.Cause`1Represents the cause of a failed workflow. The type of the domain-specific failure value.
FailAn expected domain-specific failure.
ProviderTimedOutCombine it with the built-in services
A 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:
type FullAppEnv =
{ Runtime: BaseRuntime
Rates: IExchangeRates }
interface IHasClock with
member this.Clock = this.Runtime.Clock
interface IHasExchangeRates with
member this.ExchangeRates = this.Rates
FsLiveDocsGeneratedPage9_7555C511A9CE.FullAppEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Rates: IExchangeRatesFsLiveDocsGeneratedPage9_7555C511A9CE.IExchangeRatesAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: FullAppEnvClock: FullAppEnv -> unit -> IClockClock: IClockFsLiveDocsGeneratedPage9_7555C511A9CE.IHasExchangeRatesExchangeRates: FullAppEnv -> unit -> IExchangeRatesUse a test double
type FixedRates(rate: decimal) =
interface IExchangeRates with
member _.GetUsdToAud() = Task.FromResult rate
FsLiveDocsGeneratedPage9_7555C511A9CE.FixedRatesrate: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage9_7555C511A9CE.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.
Now every workflow that depends on IExchangeRates can run against the same deterministic test implementation,
whether it is running alone or as part of the full AppEnv above.
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.
With the contract in place, helpers written for one workflow can be shared across workflows.

