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.JavaScript

.NET Hosting

Axial.Hosting connects root Flow applications to .NET process and Generic Host lifecycle. It also adapts Microsoft.Extensions.Logging to the explicit ILog service and provides fiber-defect logging.

dotnet add package Axial.Hosting

App works in console, desktop, test, and embedded applications without this package, Microsoft.Extensions.Hosting, or a dependency-injection container.

Standalone CLI or Script

Use DotNetApp.run when the application owns a console process but does not use Generic Host:

Show page setup
// Setup for the checked examples on this page.
open Axial
open Axial.Hosting
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 actual
Verified
open Axial
open Axial.Hosting

type AppError = | InvalidArguments of string

let describeError = function
    | InvalidArguments message -> message

let application : Flow<string array, AppError, unit> =
    flow {
        let! args = Flow.env<string array, AppError>
        if Array.isEmpty args then
            return! Flow.fail (InvalidArguments "Supply at least one argument.")
    }

// In a program, mark this [<EntryPoint>].
let main args =
    (DotNetApp.run describeError args application).GetAwaiter().GetResult()
Requires & types
Requires: open Axial; open Axial.Hosting (packages: Axial, Axial.Hosting)
val describeError: AppError -> string
val application: Flow<string array,AppError,unit>
val main: string array -> int
Verified
main [| "orders.csv" |] |> shouldEqual 0
main [||] |> shouldEqual 1

DotNetApp.run installs a temporary Console.CancelKeyPress handler. Ctrl+C requests App.Stop(), waits for root scope cleanup, removes the handler, and returns:

Exit Process code
Success 0
Typed failure only 1
Defect 2
Interruption 130

A cleanup defect takes precedence when a cause also contains interruption.

It returns the code rather than calling Environment.Exit, so finally blocks and asynchronous finalizers are not skipped.

Generic Host

Build the application as a Flow whose input is either IServiceProvider or an explicit environment constructed from it:

Fragment
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Axial
open Axial.Layers
open Axial.Hosting

type AppError =
    | OrderError of string
    static member describe = function OrderError message -> message

type IOrderRepository =
    abstract ProcessPending: unit -> Result<unit, AppError>

type SqlOrderRepository() =
    interface IOrderRepository with
        member _.ProcessPending() = Ok()

type AppEnv =
    { Orders: IOrderRepository
      Log: ILog }

let program : Flow<AppEnv, AppError, unit> =
    flow {
        let! orders = Flow.envWith _.Orders
        return! orders.ProcessPending()
    }

let builder = Host.CreateApplicationBuilder(args)

builder.Services.AddSingleton<IOrderRepository, SqlOrderRepository>()
|> ignore

// The layer turns the container into the explicit AppEnv the domain flow needs.
let appLayer : Layer<IServiceProvider, AppError, AppEnv> =
    Layer.envWith (fun services ->
        { Orders = services.GetRequiredService<IOrderRepository>()
          Log = services.GetRequiredService<ILoggerFactory>() |> MicrosoftLogging.fromFactory "App" })

let application : Flow<IServiceProvider, AppError, unit> =
    program
    |> Layer.provide appLayer

builder.Services
|> Hosting.addApp id AppError.describe application
|> ignore

builder.Build().Run()

The registered FlowHostedService:

  1. Constructs the explicit environment when the host starts.
  2. Starts one root App.
  3. Connects IHostApplicationLifetime.ApplicationStopping to coordinated stop.
  4. Logs success, typed failure, interruption, or defects through the host logger.
  5. Requests host shutdown when the finite root application completes.
  6. Waits for root cleanup from StopAsync.

Use Hosting.addAppWith { StopHostOnCompletion = false } when the root Flow is one hosted participant and another hosted service owns process completion.

IServiceProvider stays at the application edge. Prefer constructing an explicit record or providing a Layer before domain workflows run. See Providing the environment.

Microsoft Logging as ILog

Create the explicit Axial logging service from an existing logger:

Verified
let axialLogFor (logger: Microsoft.Extensions.Logging.ILogger) : ILog =
    MicrosoftLogging.create logger
Requires & types
Requires: open Axial.Hosting; open Axial.PlatformService (packages: Axial.Hosting, Axial.PlatformService)
val axialLogFor: Extensions.Logging.ILogger -> ILog

Or choose a category through a factory:

Verified
let axialLogFrom (loggerFactory: Microsoft.Extensions.Logging.ILoggerFactory) : ILog =
    MicrosoftLogging.fromFactory "MyApp" loggerFactory
Requires & types
Requires: open Axial.Hosting; open Axial.PlatformService (packages: Axial.Hosting, Axial.PlatformService)
val axialLogFrom: Extensions.Logging.ILoggerFactory -> ILog

MicrosoftLogging.layer "MyApp" provisions ILog from an ILoggerFactory layer input. All Axial log levels and exception objects are preserved. The adapter never silently substitutes a no-op logger.

Fiber Defect Logging

Install the observer once around the root application:

Verified
let observed (logger: Microsoft.Extensions.Logging.ILogger) (application: Flow<unit, string, unit>) =
    application
    |> FiberLogging.observe logger
Requires & types
Requires: open Axial; open Axial.Hosting (packages: Axial, Axial.Hosting)
val observed: Extensions.Logging.ILogger -> Flow<unit,string,unit> -> Flow<unit,string,unit>

Fiber defects are errors; unobserved fiber defects are critical entries. Compose it with telemetry when both are required:

Fragment
let observedWithSpans (logger: Microsoft.Extensions.Logging.ILogger) (application: Flow<unit, string, unit>) =
    application
    |> Flow.withFiberObserver
        (FiberObserver.compose
            FiberTelemetry.observerWithSpans
            (FiberLogging.observer logger))

Logging is an explicit application dependency; telemetry remains runtime instrumentation. See Observability.

Desktop and Embedded Applications

Desktop frameworks already own application lifetime. Start App after startup and await stop from the framework's closing path:

Verified
let startDesktop (requestExit: unit -> unit) (application: Flow<unit, string, unit>) =
    let running = App.start () application

    let closeApplication () = async {
        let! _ = running.Stop()
        requestExit ()
    }

    closeApplication
Requires & types
Requires: open Axial (packages: Axial)
val startDesktop: (unit -> unit) -> Flow<unit,string,unit> -> unit -> Async<unit>

Do not block the UI thread on Completion. A framework-specific package is unnecessary for wiring one close event to Stop().

Other Runtimes