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.JavaScriptEffect-boundary guardrails
Axial models operational effects as explicit, mockable dependencies. Direct access to ambient state can bypass those dependencies without changing a function's signature.
For example, Schedule.jittered once created its own System.Random, and FiberDump once read DateTimeOffset.UtcNow directly. Both errors compiled because the build did not enforce the effect boundary.
Axial.Guardrails is an FSharp.Analyzers.SDK analyzer package. It checks effect boundaries and related Axial conventions during each build.
Install the analyzer
Add the package to your project:
dotnet add package Axial.Guardrails
The package configures the analyzer automatically. You don't need to edit an MSBuild file or run a separate command. Which rules run depends on the Axial packages the project references; see Choose guardrails per package.
Findings are warnings by default. To make findings fail the build, set the severity to error:
<PropertyGroup>
<AxialGuardrailsSeverity>error</AxialGuardrailsSeverity>
</PropertyGroup>
To disable the analyzer for one project, set:
<PropertyGroup>
<AxialGuardrailsEnabled>false</AxialGuardrailsEnabled>
</PropertyGroup>
For adoption guidance, see Installation.
Run the analyzer in the Axial repository
Axial's Directory.Build.targets runs the local analyzer for every project. It imports the same targets file that the NuGet package provides, but points it at the local build output.
As a result, dotnet build and dotnet test use the same checks in this repository and in consumer projects. No separate script or CI task is required.
The repository sets AxialGuardrailsSeverity to error. To change the severity for one command, set the AXIAL_GUARDRAILS_SEVERITY environment variable.
Review the diagnostics
| Code | Name | Detects |
|---|---|---|
AXG001 |
EffectBoundary |
Direct calls to ambient .NET effects |
AXG002 |
SuppressionIntegrity |
Invalid or unused effect suppressions |
AXG003 |
RaiseInFlow |
Direct exception-raising calls inside flow { } |
AXG004 |
Fixture |
Shared module-level values in xUnit test modules |
AXG005 |
DiscardedCancellation |
Task adapters that discard their cancellation token |
AXG006 |
ReflectionFormatting |
Formatting that needs F# reflection, which NativeAOT and trimming remove |
AXG001, AXG002, AXG003, AXG005, and AXG006 run in application and library projects. They don't run in test projects, where direct effects and exceptions are often necessary for setup and assertions. Set AxialGuardrailsInTests to all to run them in a test project too.
AXG004 runs only in projects where MSBuild sets IsTestProject. It checks a test-specific risk and does not apply to application or library projects.
Choose guardrails per package
The rules are grouped into guardrails, each named after the package whose service replaces the effect it flags.
Referencing a package turns its guardrail on: a rule that recommends IHttp is only useful once IHttp is available.
| Guardrail | Rules |
|---|---|
Axial |
Ambient clock reads, Stopwatch, Task.Delay, Thread.Sleep, Environment.ProcessorCount, blocking task waits inside flow { }, and AXG002–AXG006 |
Axial.PlatformService |
Randomness, GUIDs, and environment state |
Axial.Console |
System.Console |
Axial.FileSystem |
System.IO.File and System.IO.Directory |
Axial.Process |
Process.Start |
Axial.HttpClient |
new HttpClient |
Change the set with ordinary MSBuild item operations in a project or Directory.Build.props:
<ItemGroup>
<!-- Turn a guardrail off -->
<AxialGuardrail Remove="Axial.Console" />
<!-- Give one guardrail its own severity; the others use AxialGuardrailsSeverity -->
<AxialGuardrail Update="Axial.HttpClient" Severity="warning" />
<!-- Turn one on without referencing its package -->
<AxialGuardrail Include="Axial.FileSystem" />
</ItemGroup>
Add rules for your own package
A package can contribute rules to a guardrail of its own. Ship them in the package's buildTransitive props, next to
the item that turns the guardrail on:
<ItemGroup>
<AxialGuardrail Include="Acme.Payments" />
<AxialGuardrailRule Include="acme.gateway-client"
Guardrail="Acme.Payments"
Category="payments"
Match="ctor:Acme.Gateway.GatewayClient"
Scope="anywhere"
Message="constructs the gateway client directly, which bypasses the payments service"
Replacement="Acme.Payments' IPayments service" />
</ItemGroup>
Match is ctor:Type, member:Type::Member1,Member2, or any:Type. Scope is anywhere, or insideFlow to
report the call only inside flow { }. Findings are reported as AXG001, and // axial-allow-effect: payments allows
an intentional use. Item metadata cannot contain a semicolon; write it as %3B.
AXG001: Use explicit effect services
AXG001 detects direct calls to ambient .NET effects.
| Category | Detects | Use instead |
|---|---|---|
random |
Construction of System.Random |
IRandom, Random.service, or Random.nextDouble |
guid |
Guid.NewGuid() |
IGuid, Guid.service, or Guid.newGuid |
clock |
Ambient date and time properties, Stopwatch, and Task.Delay |
IClock, Clock.service, Clock.utcNow, Clock.timed, Flow.sleep, or Schedule |
environment |
Ambient environment variables and machine or process properties | IEnvironment or a value passed through 'env; Parallelism.ofProcessors for ProcessorCount |
console |
Any System.Console member |
Axial.Console.IConsole |
filesystem |
Any System.IO.File or System.IO.Directory member |
Axial.FileSystem.IFileSystem |
process |
System.Diagnostics.Process.Start |
Axial.Process.IProcess |
sleep |
Thread.Sleep |
Flow.sleep or Schedule |
http |
Construction of HttpClient |
Axial.HttpClient.IHttp, with Http.live over one shared client |
blocking |
.GetAwaiter().GetResult() and .Result on a task, inside flow { } only |
let! on the task, or Flow.fromBlocking for synchronous work |
The analyzer matches resolved System.* symbols, not source text. It does not flag an application type named Random or a local value named now.
Allow an intentional effect boundary
Some code implements the explicit boundary around an effect. Examples include a live service implementation, a process entry point, and the scheduler.
To allow one call, add a category-specific directive to the flagged line or the line immediately above it:
let live : IClock =
let stopwatch = System.Diagnostics.Stopwatch.StartNew() // axial-allow-effect: clock
{ new IClock with
member _.UtcNow() = DateTimeOffset.UtcNow // axial-allow-effect: clock
member _.Elapsed() = stopwatch.Elapsed
member _.Sleep(delay, token) = Task.Delay(delay, token) }
live: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
stopwatch: Diagnostics.StopwatchSystemStartNew: unit -> Diagnostics.StopwatchInitializes a new instance, sets the elapsed time property to zero, and starts measuring elapsed time. A that has just begun measuring elapsed time.
DiagnosticsSystem.Diagnostics.StopwatchProvides a set of methods and properties that you can use to accurately measure elapsed time.
_: IClockUtcNow: unit -> DateTimeOffsetSystem.DateTimeOffsetRepresents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).
UtcNow: DateTimeOffsetGets a object whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is . An object whose date and time is the current Coordinated Universal Time (UTC) and whose offset is .
Elapsed: unit -> TimeSpanElapsed: TimeSpanGets the total elapsed time measured by the current instance. A read-only representing the total elapsed time measured by the current instance.
Sleep: TimeSpan * CancellationToken -> Taskdelay: TimeSpantoken: CancellationTokenSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
Delay: TimeSpan * CancellationToken -> TaskCreates a cancellable task that completes after a specified time interval. The time span to wait before completing the returned task, or to wait indefinitely. A cancellation token to observe while waiting for the task to complete. A task that represents the time delay. represents a negative time interval other than . -or- The argument's property is greater than . The task has been canceled. The provided has already been disposed.
To allow a category throughout a boundary implementation file, place a file directive in the leading comment block immediately before the namespace declaration:
// This file provides the live IConsole implementation.
// axial-allow-effect-file: console
A file directive allows only the named categories. For example, a console directive does not allow System.Random().
Each directive must name a category. There is no directive that allows every effect.
For the exact matching and suppression rules, see EffectCatalog.fs and Suppressions.fs in src/Axial.Guardrails.
AXG002: Remove invalid suppressions
AXG002 validates every axial-allow-effect and axial-allow-effect-file directive.
It reports a directive when the category is unknown or when the directive does not suppress an AXG001 finding. This check catches spelling errors and directives left behind after a refactoring. A directive whose guardrail is turned off suppresses nothing, so it is reported too.
Remove an unused directive. Correct a category only when the associated call is an intentional effect boundary.
AXG003: Return typed failures from workflows
A Flow<'env, 'error, 'value> represents expected failures through its 'error channel. Raising an exception inside flow { } creates an unhandled defect instead, so callers cannot handle it as an expected error.
AXG003 detects direct calls to raise, failwith, failwithf, invalidOp, invalidArg, and reraise inside flow { }. It also checks nested conditionals, matches, bindings, and lambdas.
Use return! Flow.fail error for an expected failure. Use Flow.die when the failure is an unrecoverable defect.
If a nearby try/with catches and translates the exception, add axial-allow-raise to the flagged line or the line immediately above it:
let start (proc: System.Diagnostics.Process) =
if not (proc.Start()) then raise (Exception "Process did not start") // axial-allow-raise
start: Diagnostics.Process -> unitproc: Diagnostics.ProcessSystemSystem.Diagnostics.ProcessProvides access to local and remote processes and enables you to start and stop local system processes.
Diagnostics``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
Start: unit -> boolStarts (or reuses) the process resource that is specified by the property of this component and associates it with the component. if a process resource is started; if no new process resource is started (for example, if an existing process is reused). No file name was specified in the component's . -or- The member of the property is while , , or is . There was an error in opening the associated file. The process object has already been disposed. Method not supported on operating systems without shell support such as Nano Server (.NET Core only).
raise: Exception -> 'TRaises an exception The exception to raise. The result value. open System.IO exception FileNotFoundException of string let readFile (fileName: string) = if not (File.Exists(fileName)) then raise(FileNotFoundException(fileName)) File.ReadAllText(fileName) readFile "/this-file-doest-exist" When executed, raises a FileNotFoundException.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
Use this directive only when the same local boundary catches and translates the exception.
AXG004: Create fresh test fixtures
A module-level let value is initialized once and shared by every test in the module. Mutable or stateful values can therefore make parallel xUnit tests interfere with each other.
AXG004 checks modules that contain an xUnit [<Fact>] or [<Theory>], or an FsCheck [<Property>]. It reports module-level values but not functions such as let fixture () = ....
The analyzer excludes constants and point-free function definitions because they do not create the shared-state risk.
Create fixtures inside each test or expose a function that returns a fresh fixture. For a safe immutable value, add axial-allow-fixture to the flagged line or the line immediately above it:
let private syntheticTime = DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero) // axial-allow-fixture
syntheticTime: DateTimeOffset``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
AXG005: Preserve cancellation tokens
Task adapters receive Flow's cancellation token. Discarding that token prevents cancellation from stopping the underlying operation.
AXG005 checks ColdTask, ColdTask.create, Flow.fromTask, and Flow.fromTaskResult. It reports a single-argument lambda when the argument is a bare discard, such as fun _ -> ....
Use the cancellation-aware overload of the wrapped operation and pass the token to it.
If a legacy API has no cancellation-aware overload, add axial-allow-discarded-cancellation to the flagged line or the line immediately above it:
/// An API with no cancellation support.
let legacyCall () : Task<int> = Task.FromResult 42
let legacy = ColdTask(fun _ -> legacyCall ()) // axial-allow-discarded-cancellation
legacyCall: unit -> Task<int>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
intAn abbreviation for the CLI type . Basic Types
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.
legacy: ColdTask<int>ColdTaskThis check does not run in test projects. Test adapters often wrap completed tasks that have no work to cancel.
AXG006: Format without reflection
F# unions and records get a compiler-generated ToString() that calls sprintf "%+A". %A walks the value with
FSharp.Reflection, and FSharp.Core's option, voption, list, Result, Choice, Map, and Set print their
contents the same way. NativeAOT and full trimming remove the metadata this needs. The call then throws or prints the
wrong text, and the build gives no specific warning because the reflection happens inside FSharp.Core.
AXG006 reports these formatting sites in typed code:
x.ToString(),string x, and interpolation holes such as$"{x}"whenxis an F# union, record, anonymous record, exception, or one of the FSharp.Core types above, and the type has no hand-writtenToStringoverride- the same forms on a type parameter, whose substituted type may be a union or record
%Aon any value other than a primitive, even when its type overridesToString- a boxed value of those types passed to
String.Format,String.Concat,StringBuilder.Append,TextWriter.Write,Console.Write,Trace.WriteLine, orDebug.WriteLine
Render the value explicitly instead. Match on the cases, call a describe function, or give the type a hand-written
override:
type OrderError =
| OutOfStock of sku: string
| InvalidQuantity of int
override this.ToString() =
match this with
| OutOfStock sku -> $"Out of stock: {sku}"
| InvalidQuantity quantity -> $"Invalid quantity: {quantity}"
FsLiveDocsGeneratedPage10_7555C511A9CE.OrderErrorOutOfStocksku: stringstringAn abbreviation for the CLI type . Basic Types
InvalidQuantityintAn abbreviation for the CLI type . Basic Types
this: OrderErrorToString: OrderError -> unit -> stringquantity: intWith that override, $"{error}", string error, and %O are safe. %A is not.
Axial's own public unions render themselves this way, including Exit, Cause, FiberStatus, FiberId, and the
Process, HttpClient, FileSystem, and PlatformService error types. A payload inside Exit or Cause is still
rendered with its own ToString, so give your error types an override too. Cause.prettyPrint also accepts an
explicit error renderer.
If code never runs trimmed, such as a script or a test helper, add axial-allow-reflection-format to the flagged line
or the line immediately above it:
let printDiagnostics (diagnostics: Map<string, int list>) =
printfn "%A" diagnostics // axial-allow-reflection-format
printDiagnostics: Map<string,int list> -> unitdiagnostics: Map<string,int list>Microsoft.FSharp.Collections.FSharpMap`2Immutable maps based on binary trees, where keys are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on key values. See the module for further operations on maps. All members of this class are thread-safe and may be used concurrently from multiple threads.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
printfn: 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.
scripts/run-aot-probe.sh publishes a NativeAOT probe that renders these types and dumps a fiber registry, so the
release checks catch a regression that the analyzer cannot see.

