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.JavaScriptAxial.FileSystem turns file access into a declared dependency with a typed failure channel. Where
File.ReadAllText throws one of a dozen exception types, FileSystem.readAllText returns
Flow<'env, FileSystemError, string> — the ways it can fail are part of the signature.
open Axial.FileSystem
let loadConfig path : Flow<#IHasFileSystem, FileSystemError, Config> =
flow {
let! text = FileSystem.readAllText path
return parseConfig text
}Supplying the service
IFileSystem is supplied the same way as any other explicit service:
type AppEnv =
{ FileSystem: IFileSystem }
interface IHasFileSystem with
member this.FileSystem = this.FileSystem
let! exit = loadConfig "app.json" |> Flow.startTask { FileSystem = FileSystem.live }For a runtime assembled with layers, wrap it: Layer.succeed FileSystem.live.
Typed errors
Every operation fails with FileSystemError, a union that classifies what went wrong:
| Case | Raised when |
|---|---|
FileNotFound path |
The file does not exist |
DirectoryNotFound path |
A directory in the path does not exist |
AlreadyExists path |
The target path is already taken |
Unauthorized (path, message) |
The process lacks permission |
InvalidPath (path, message) |
The path is malformed |
PathTooLong (path, message) |
The platform rejected the path length |
Canceled message |
The operation was interrupted |
Io (path, message) |
A general I/O failure |
Unsupported (path, message) |
The platform or path shape does not support the operation |
Unexpected (path, message) |
Anything else that escaped the operation |
Because failures are typed, recovery is a match rather than an exception filter:
let loadOrDefault path =
loadConfig path
|> Flow.orElseWith (function
| FileSystemError.FileNotFound _ -> Flow.succeed Config.defaults
| error -> Flow.fail error)FileSystemError.describe formats a case for logs and messages. FileSystemError.fromException performs the
classification itself, which is useful when adapting a third-party API into the same error type.
Files
Whole-file reads and writes come in text, line, and byte forms, each with an encoding-explicit and an asynchronous variant:
FileSystem.readAllText path
FileSystem.readAllTextWithEncoding Encoding.UTF8 path
FileSystem.readAllTextAsync path
FileSystem.readAllLines path
FileSystem.readAllBytes path
FileSystem.writeAllText path contents
FileSystem.writeAllLines path lines
FileSystem.writeAllBytes path bytes
FileSystem.appendAllText path contentsThe Async variants pass the flow's cancellation token to the underlying call, so an interrupted workflow stops a
large read in progress rather than after it. Prefer them for anything that is not small.
fileExists, exists, deleteFile, copyFile, and moveFile cover the rest of the common surface, and file
metadata has getters and setters for attributes and the creation, last-access, and last-write times in both local and
UTC forms — getFileLastWriteTimeUtc, setFileAttributes, and so on.
Symbolic links are first class: createFileSymbolicLink, createDirectorySymbolicLink, getSymbolicLinkTarget
(which returns None when the path is not a link), and resolveSymbolicLinkTarget, whose boolean argument decides
whether to follow the whole chain or stop at the immediate target.
Streams and scopes
openRead, openText, openWrite, createFile, createText, appendText, and the openFile family return open
handles. An open handle is a resource, so acquire it inside a scope rather than trusting a later Dispose:
let copyThrough source destination =
Flow.acquireReleaseWith
(FileSystem.openRead source)
(fun stream _ ->
stream.Dispose()
Task.CompletedTask)
(fun stream -> readAndTransform stream destination)Cleanup then runs whether the workflow succeeds, fails, defects, or is interrupted. See scopes and resources.
Directories and paths
createDirectory creates missing parents. deleteDirectory path recursive takes the recursion flag explicitly, so a
recursive delete is visible at the call site. Listing comes in eager (getFiles, getDirectories,
getFileSystemEntries) and lazy (enumerateFiles, enumerateDirectories, enumerateFileSystemEntries) forms, each
taking a search pattern and a SearchOption:
let fsharpSources root =
FileSystem.enumerateFiles root "*.fs" SearchOption.AllDirectories
fsharpSources: string -> Flow<'a,FileSystemError,string seq>root: stringAxial.FileSystem.FileSystemModuleenumerateFiles: string -> string -> SearchOption -> Flow<'env,FileSystemError,string seq>Enumerates files through an explicit file-system service.
System.IO.SearchOptionSpecifies whether to search the current directory, or the current directory and all subdirectories.
AllDirectories: SearchOptionIncludes the current directory and all its subdirectories in a search operation. This option includes reparse points such as mounted drives and symbolic links in the search.
Path manipulation is also on the service — combine, getFullPath, getFileName, getExtension, getRelativePath,
getTempPath, getRandomFileName, and the rest. These are pure string operations on .NET, but routing them through
the service keeps platform-specific separator and rooting behaviour substitutable in tests.
Testing
IFileSystem is a wide interface, and implementing it in full to fake three calls is rarely worth it. Two approaches
work better:
Use FileSystem.live against a temporary directory. This is what Axial's own tests do. The workflow exercises
real I/O, and the test owns cleanup:
let root = Path.Combine(Path.GetTempPath(), "my-tests", Guid.NewGuid().ToString "N")
Directory.CreateDirectory root |> ignore
try
let exit = workflow root |> Flow.run { FileSystem = FileSystem.live }
test <@ exit = Exit.Success expected @>
finally
Directory.Delete(root, true)Wrap FileSystem.live to inject one failure. When the point of the test is error handling, delegate every member
to the live service and override the one that should fail — that keeps the fake honest about everything else.
Fable
FileSystem.live is not compiled for Fable, and Layer.succeed FileSystem.live fails with PlatformNotSupportedException there.
See packages and platforms.
Related
- Service contracts — how a package declares the service it needs.
- Scopes and resources — deterministic cleanup for open handles.
- Error handling — expected failures against defects.

