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
Built-in services use a narrow interface (IClock, IFileSystem), an IHasX marker the environment implements,
and helpers constrained by that marker rather than by a field name. This tutorial uses the same shape for a
currency conversion rate.
Use a named contract when several workflows need the same dependency without relying on one app record field name.
Built-in services use IHasX for this reason. A dependency used by exactly one workflow
usually does not need a contract; see choosing an approach.
Define the contract
Show page setup
// Setup for the checked examples on this page.
open System
open System.Threading.Tasks
open Axial
open Axial.PlatformService
/// 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 System.Threading.Tasks
type IExchangeRates =
abstract GetUsdToAud : unit -> Task<decimal>
SystemThreadingTasksFsLiveDocsGeneratedPage2_6B7179067CFE.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
Requires & types
Requires: open System.Threading.TasksMake 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
}
FsLiveDocsGeneratedPage2_6B7179067CFE.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRatesFsLiveDocsGeneratedPage2_6B7179067CFE.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
FsLiveDocsGeneratedPage2_6B7179067CFE.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>enverrorAxial.Flow`3Axial.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
Requires & types
Requires: open Axial (packages: Axial)val service: Flow<'env,'error,IExchangeRates>val priceInAud: decimal -> Flow<'env,'error,decimal>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. For typed failures, compare
FileSystemError or HttpError:
type ExchangeRateError =
| RateUnavailable of message: string
| ProviderTimedOut
module ExchangeRateError =
let fromException (error: exn) =
match error with
| :? TimeoutException -> ProviderTimedOut
| other -> RateUnavailable other.Message
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 ExchangeRateError.fromException
return usdAmount * rate
}
FsLiveDocsGeneratedPage2_6B7179067CFE.ExchangeRateErrorRateUnavailablemessage: stringstringAn abbreviation for the CLI type . Basic Types
ProviderTimedOutFsLiveDocsGeneratedPage2_6B7179067CFE.ExchangeRateErrorModulefromException: exn -> ExchangeRateErrorerror: exnexnAn abbreviation for the CLI type . Basic Types
System.TimeoutExceptionThe exception that is thrown when the time allotted for a process or operation has expired.
other: exnMessage: stringGets a message that describes the current exception. The error message that explains the reason for the exception, or an empty string ("").
priceInAudOrFail: decimal -> Flow<'env,ExchangeRateError,decimal>envFsLiveDocsGeneratedPage2_6B7179067CFE.IHasExchangeRatesusdAmount: decimaldecimalAn abbreviation for the CLI type . Basic Types
Axial.Flow`3flow: FlowBuilderThe universal flow { } computation expression.
rates: IExchangeRatesFsLiveDocsGeneratedPage2_6B7179067CFE.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
Requires & types
Requires: open Axial; open System (packages: Axial)val fromException: exn -> ExchangeRateErrorval priceInAudOrFail: decimal -> Flow<'env,ExchangeRateError,decimal>Flow.attemptTask routes a thrown exception into the typed error channel as exn; fromException classifies it so
a timeout becomes ProviderTimedOut and any other exception becomes RateUnavailable carrying its message. A
caller can now match either case as an ordinary value instead of catching an exception.
Provide an app environment
type AppEnv =
{ Rates: IExchangeRates
Region: string }
interface IHasExchangeRates with
member this.ExchangeRates = this.Rates
FsLiveDocsGeneratedPage2_6B7179067CFE.AppEnvRates: IExchangeRatesFsLiveDocsGeneratedPage2_6B7179067CFE.IExchangeRatesRegion: stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage2_6B7179067CFE.IHasExchangeRatesthis: AppEnvExchangeRates: AppEnv -> unit -> IExchangeRateslet fixedRate = { new IExchangeRates with member _.GetUsdToAud() = Task.FromResult 1.5m }
let timedOut = { new IExchangeRates with member _.GetUsdToAud() = Task.FromException<decimal>(TimeoutException()) }
let unavailable = { new IExchangeRates with member _.GetUsdToAud() = Task.FromException<decimal>(Exception("no rate")) }
priceInAud 10m |> Flow.run { Rates = fixedRate; Region = "au" } |> shouldEqual (Exit.Success 15.0m : Exit<decimal, string>)
priceInAudOrFail 10m |> Flow.run { Rates = timedOut; Region = "au" } |> shouldEqual (Exit.Failure(Cause.Fail ProviderTimedOut))
priceInAudOrFail 10m |> Flow.run { Rates = unavailable; Region = "au" } |> shouldEqual (Exit.Failure(Cause.Fail (RateUnavailable "no rate")))
fixedRate: IExchangeRatesFsLiveDocsGeneratedPage2_6B7179067CFE.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.
timedOut: 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.
unavailable: IExchangeRates``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
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.
ProviderTimedOutRateUnavailableRequires & types
Requires: open Axial; open System.Threading.Tasks (packages: Axial)val fixedRate: IExchangeRatesval timedOut: IExchangeRatesval unavailable: IExchangeRatesCombine it with the built-in services
A custom service and BaseRuntime can share an environment: each contract gets its own interface member,
delegating to the stored value. 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
FsLiveDocsGeneratedPage2_6B7179067CFE.FullAppEnvRuntime: BaseRuntimeAxial.PlatformService.BaseRuntimeGroups the standard operational services commonly used by workflow hosts.
Rates: IExchangeRatesFsLiveDocsGeneratedPage2_6B7179067CFE.IExchangeRatesAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
this: FullAppEnvClock: FullAppEnv -> unit -> IClockClock: IClockFsLiveDocsGeneratedPage2_6B7179067CFE.IHasExchangeRatesExchangeRates: FullAppEnv -> unit -> IExchangeRatesRequires & types
Requires: open Axial; open Axial.PlatformService (packages: Axial, Axial.PlatformService)Use a test double
type FixedRates(rate: decimal) =
interface IExchangeRates with
member _.GetUsdToAud() = Task.FromResult rate
FsLiveDocsGeneratedPage2_6B7179067CFE.FixedRatesrate: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage2_6B7179067CFE.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.
Requires & types
Requires: open System.Threading.TasksWorkflows that depend on IExchangeRates can use this deterministic test implementation alone or as part of the
full AppEnv above.
Publish it from a package
The examples above define the service in the application. To ship the contract, helper module, and a live
implementation to other applications (as Axial.FileSystem does for Axial.Core), see
providing services from a package.

