Frank.Statecharts.Core 7.3.0

dotnet add package Frank.Statecharts.Core --version 7.3.0
                    
NuGet\Install-Package Frank.Statecharts.Core -Version 7.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Frank.Statecharts.Core" Version="7.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Frank.Statecharts.Core" Version="7.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Frank.Statecharts.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Frank.Statecharts.Core --version 7.3.0
                    
#r "nuget: Frank.Statecharts.Core, 7.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Frank.Statecharts.Core@7.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Frank.Statecharts.Core&version=7.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Frank.Statecharts.Core&version=7.3.0
                    
Install as a Cake Tool

Frank

NuGet Version GitHub Release Date Build status

A web framework for building applications where resources are the primary abstraction, invalid states are structurally impossible, and the application itself is the API documentation. Frank uses F# computation expressions as a declarative, extensible layer over ASP.NET Core.

Frank is built on four ideas:

Resources, not routes. HTTP resources are the unit of design. You define what a resource is and what it can do — the framework handles routing, method dispatch, and metadata. This is REST as Fielding described it, not the "REST" that became a synonym for JSON-over-HTTP.

Make invalid states impossible. Statechart-enforced state machines govern resource behavior at the framework level. If a transition isn't legal, it isn't available — in the response headers, in the HTML controls, in the API surface. No defensive coding required.

Built for the age of agents. Frank provides CLI tooling and extension libraries that layer semantic metadata onto your application — ALPS profiles, Link headers, JSON Home documents, OWL ontologies. Developers and agents can reflect on a running application, understand its capabilities, and refine it continuously.

Discovery is a first-class concern. A Frank application is understandable from a cold start. JSON Home documents advertise available resources. Link headers connect them. Allow headers declare what's possible in the current state. ALPS profiles define what things mean. Semantic web vocabularies give structure a shared language. No SDK, no out-of-band documentation — the application explains itself through standard HTTP, content negotiation, and open standards that clients (human or machine) can navigate without prior knowledge.

let home =
    resource "/" {
        name "Home"

        get (fun (ctx: HttpContext) ->
            ctx.Response.WriteAsync("Welcome!"))
    }

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults
        resource home
    }
    0

What This Looks Like in Practice

When you combine statecharts, affordances, and discovery, a Frank application tells clients exactly what's possible at every point in a protocol. Here's a TicTacToe game wired with affordance middleware:

webHost args {
    useDefaults
    plug resolveStateKey           // 1. Resolve current state from store
    useAffordances gameAffordanceMap  // 2. Inject Link + Allow headers per state
    useStatecharts                 // 3. Dispatch to state-specific handlers

    resource gameResource          // GET /games/{gameId}, POST /games/{gameId}
    resource sseResource           // GET /games/{gameId}/sse (Datastar SSE)
}

When an agent hits /games/42 during X's turn, it gets back:

Allow: GET, POST
Link: </games/42>; rel="self", </games/42>; rel="makeMove"; method="POST"
Link: </alps/games>; rel="profile"

When the game is won, the response changes:

Allow: GET
Link: </games/42>; rel="self"
Link: </alps/games>; rel="profile"

No special client library. No out-of-band documentation. The API tells you what's possible right now, and the framework guarantees the response is correct.


Getting Started

Frank was inspired by @filipw's Building Microservices with ASP.NET Core (without MVC).


Packages

Package Description NuGet
Frank Core computation expressions for WebHost and routing NuGet
Frank.Auth Resource-level authorization extensions NuGet
Frank.OpenApi Native OpenAPI document generation with F# type schemas NuGet
Frank.Datastar Datastar SSE integration for reactive hypermedia NuGet
Frank.LinkedData Semantic RDF/Linked Data content negotiation middleware NuGet
Frank.Cli.MSBuild MSBuild integration for auto-embedding semantic artifacts NuGet
Frank.Resources.Model Pure resource model types: type analysis, capabilities, affordances, and runtime projections (zero dependencies) NuGet
Frank.Statecharts.Core Shared statechart AST types for format parsers and generators (zero dependencies) NuGet
Frank.Statecharts Stateful resources with state machines, guards, affordance middleware (Link + Allow headers), multi-format parsers/generators (WSD, ALPS, SCXML, smcat, XState), and cross-format validation NuGet
Frank.Discovery OPTIONS-based link discovery and Link header middleware NuGet
Frank.Analyzers F# Analyzers for compile-time error detection NuGet
Frank.Validation SHACL shape validation derived from F# types NuGet
Frank.Provenance PROV-O provenance tracking for resource state changes NuGet

Package Dependency Graph

Frank (core)
│   ETag / conditional request middleware
│
├── Frank.Resources.Model ────── (zero dependencies)
│   └── Resource types, affordance map, runtime projections
│
├── Frank.Auth
│
├── Frank.LinkedData ──────────── Frank
│
├── Frank.OpenApi ─────────────── Frank
│
├── Frank.Datastar ────────────── Frank
│
├── Frank.Statecharts.Core ────── (zero dependencies)
│   └── Shared statechart AST (StatechartDocument, StateNode, TransitionEdge, Annotation, ParseResult)
│
├── Frank.Statecharts ─────────── Frank + Frank.Resources.Model + Frank.Statecharts.Core
│   └── WSD, ALPS, SCXML, smcat, XState parsers/generators
│   └── Cross-format validation pipeline
│   └── Affordance middleware (Link + Allow headers per state)
│   └── Profile discovery (/.well-known/frank-profiles, ALPS/OWL/SHACL/JSON Schema endpoints)
│
├── Frank.Discovery ───────────── Frank
│   └── HTTP-level discovery: OPTIONS/Allow headers, RFC 8288 Link headers
│
├── Frank.Validation ──────────── Frank.LinkedData + Frank.Auth
│
├── Frank.Provenance ──────────── Frank.LinkedData + Frank.Statecharts
│
└── Frank.Sparql (planned) ────── Frank.LinkedData + Frank.Provenance

Features

  • WebHostBuilder - computation expression for configuring WebHost
  • ResourceBuilder - computation expression for configuring resources (routing)
  • No pre-defined view engine - use your preferred view engine implementation, e.g. Falco.Markup, Oxpecker.ViewEngine, or Hox
  • Easy extensibility - just extend the Builder with your own methods!

Basic Example

module Program

open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Routing
open Microsoft.AspNetCore.Routing.Internal
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
open Frank
open Frank.Builder

let home =
    resource "/" {
        name "Home"

        get (fun (ctx:HttpContext) ->
            ctx.Response.WriteAsync("Welcome!"))
    }

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults

        logging (fun options-> options.AddConsole().AddDebug())

        plugWhen isDevelopment DeveloperExceptionPageExtensions.UseDeveloperExceptionPage
        plugWhenNot isDevelopment HstsBuilderExtensions.UseHsts

        plugBeforeRouting HttpsPolicyBuilderExtensions.UseHttpsRedirection
        plugBeforeRouting StaticFileExtensions.UseStaticFiles

        resource home
    }

    0

Middleware Pipeline

Frank provides two middleware operations with different positions in the ASP.NET Core pipeline:

Request → plugBeforeRouting → UseRouting → plug → Endpoints → Response

plugBeforeRouting

Use for middleware that must run before routing decisions are made:

  • HttpsRedirection - redirect before routing
  • StaticFiles - serve static files without routing overhead
  • ResponseCompression - compress all responses
  • ResponseCaching - cache before routing
webHost args {
    plugBeforeRouting HttpsPolicyBuilderExtensions.UseHttpsRedirection
    plugBeforeRouting StaticFileExtensions.UseStaticFiles
    resource myResource
}

plug

Use for middleware that needs routing information (e.g., the matched endpoint):

  • Authentication - may need endpoint metadata
  • Authorization - requires endpoint to check policies
  • CORS - may use endpoint-specific policies
webHost args {
    plug AuthenticationBuilderExtensions.UseAuthentication
    plug AuthorizationAppBuilderExtensions.UseAuthorization
    resource protectedResource
}

Conditional Middleware

Both plugWhen and plugWhenNot run in the plug position (after routing):

webHost args {
    plugWhen isDevelopment DeveloperExceptionPageExtensions.UseDeveloperExceptionPage
    plugWhenNot isDevelopment HstsBuilderExtensions.UseHsts
    resource myResource
}

Conditional Before-Routing Middleware

Both plugBeforeRoutingWhen and plugBeforeRoutingWhenNot run in the plugBeforeRouting position (before routing):

let isDevelopment (app: IApplicationBuilder) =
    app.ApplicationServices
        .GetService<IWebHostEnvironment>()
        .IsDevelopment()

webHost args {
    // Only redirect to HTTPS in production
    plugBeforeRoutingWhenNot isDevelopment HttpsPolicyBuilderExtensions.UseHttpsRedirection

    // Only serve static files locally in development (CDN in production)
    plugBeforeRoutingWhen isDevelopment StaticFileExtensions.UseStaticFiles

    resource myResource
}

Frank.Auth

Frank.Auth provides resource-level authorization for Frank applications, integrating with ASP.NET Core's built-in authorization infrastructure.

Installation

dotnet add package Frank.Auth

Protecting Resources

Add authorization requirements directly to resource definitions:

open Frank.Builder
open Frank.Auth

// Require any authenticated user
let dashboard =
    resource "/dashboard" {
        name "Dashboard"
        requireAuth
        get (fun ctx -> ctx.Response.WriteAsync("Welcome to Dashboard"))
    }

// Require a specific claim
let adminPanel =
    resource "/admin" {
        name "Admin"
        requireClaim "role" "admin"
        get (fun ctx -> ctx.Response.WriteAsync("Admin Panel"))
    }

// Require a role
let engineering =
    resource "/engineering" {
        name "Engineering"
        requireRole "Engineering"
        get (fun ctx -> ctx.Response.WriteAsync("Engineering Portal"))
    }

// Reference a named policy
let reports =
    resource "/reports" {
        name "Reports"
        requirePolicy "CanViewReports"
        get (fun ctx -> ctx.Response.WriteAsync("Reports"))
    }

// Compose requirements (AND semantics — all must pass)
let sensitive =
    resource "/api/sensitive" {
        name "Sensitive"
        requireAuth
        requireClaim "scope" "admin"
        requireRole "Engineering"
        get (fun ctx -> ctx.Response.WriteAsync("Sensitive data"))
    }

Application Wiring

Configure authentication and authorization services using Frank's builder syntax:

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults

        useAuthentication (fun auth ->
            // Configure your authentication scheme here
            auth)

        useAuthorization

        authorizationPolicy "CanViewReports" (fun policy ->
            policy.RequireClaim("scope", "reports:read") |> ignore)

        resource dashboard
        resource adminPanel
        resource reports
    }
    0

Authorization Patterns

Pattern Operation Behavior
Authenticated user requireAuth 401 if unauthenticated, 200 if authenticated
Claim (single value) requireClaim "type" "value" 403 if claim missing or wrong value
Claim (multiple values) requireClaim "type" ["a"; "b"] 200 if user has any listed value (OR)
Role requireRole "Admin" 403 if user not in role
Named policy requirePolicy "PolicyName" Delegates to registered policy
Multiple requirements Stack multiple require* AND semantics — all must pass
No requirements (default) Publicly accessible, zero overhead

Frank.OpenApi

Frank.OpenApi provides native OpenAPI document generation for Frank applications, with first-class support for F# types and declarative metadata using computation expressions.

Installation

dotnet add package Frank.OpenApi

HandlerBuilder Computation Expression

Define handlers with embedded OpenAPI metadata using the handler computation expression:

open Frank.Builder
open Frank.OpenApi

type Product = { Name: string; Price: decimal }
type CreateProductRequest = { Name: string; Price: decimal }

let createProductHandler =
    handler {
        name "createProduct"
        summary "Create a new product"
        description "Creates a new product in the catalog"
        tags [ "Products"; "Admin" ]
        produces typeof<Product> 201
        accepts typeof<CreateProductRequest>
        handle (fun (ctx: HttpContext) -> task {
            let! request = ctx.Request.ReadFromJsonAsync<CreateProductRequest>()
            let product = { Name = request.Name; Price = request.Price }
            ctx.Response.StatusCode <- 201
            do! ctx.Response.WriteAsJsonAsync(product)
        })
    }

let productsResource =
    resource "/products" {
        name "Products"
        post createProductHandler
    }

HandlerBuilder Operations

Operation Description
name "operationId" Sets the OpenAPI operationId
summary "text" Brief summary of the operation
description "text" Detailed description
tags [ "Tag1"; "Tag2" ] Categorize endpoints
produces typeof<T> statusCode Define response type and status code
produces typeof<T> statusCode ["content/type"] Response with content negotiation
producesEmpty statusCode Empty responses (204, 404, etc.)
accepts typeof<T> Define request body type
accepts typeof<T> ["content/type"] Request with content negotiation
handle (fun ctx -> ...) Handler function (supports Task, Task<'a>, Async<unit>, Async<'a>)

F# Type Schema Generation

Frank.OpenApi automatically generates JSON schemas for F# types:

// F# records with required and optional fields
type User = {
    Id: Guid
    Name: string
    Email: string option  // Becomes nullable in schema
}

// Discriminated unions (anyOf/oneOf)
type Response =
    | Success of data: string
    | Error of code: int * message: string

// Collections
type Products = {
    Items: Product list
    Tags: Set<string>
    Metadata: Map<string, string>
}

WebHostBuilder Integration

Enable OpenAPI document generation in your application:

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults
        useOpenApi  // Adds /openapi/v1.json endpoint

        resource productsResource
    }
    0

The OpenAPI document will be available at /openapi/v1.json.

Content Negotiation

Define multiple content types for requests and responses:

handler {
    name "getProduct"
    produces typeof<Product> 200 [ "application/json"; "application/xml" ]
    accepts typeof<ProductQuery> [ "application/json"; "application/xml" ]
    handle (fun ctx -> task { (* ... *) })
}

Backward Compatibility

Frank.OpenApi is fully backward compatible with existing Frank applications. You can:

  • Mix HandlerDefinition and plain RequestDelegate handlers in the same resource
  • Add OpenAPI metadata incrementally without changing existing code
  • Use the library only where you need API documentation

Frank.LinkedData

Frank.LinkedData provides automatic RDF content negotiation for Frank applications. Endpoints marked with linkedData can serve JSON-LD, Turtle, and RDF/XML representations alongside standard JSON — driven by an OWL ontology extracted from your F# domain types.

Installation

dotnet add package Frank.LinkedData
dotnet add package Frank.Cli.MSBuild

The Frank.Cli.MSBuild package auto-embeds semantic artifacts (ontology, SHACL shapes, manifest) into your assembly at build time.

Marking Resources

Add linkedData to any resource to enable RDF content negotiation:

open Frank.Builder
open Frank.LinkedData

let products =
    resource "/products" {
        name "Products"
        linkedData
        get (fun ctx -> ctx.Response.WriteAsJsonAsync(getAllProducts()))
    }

Application Wiring

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults
        useLinkedData  // Loads embedded ontology and enables content negotiation

        resource products
    }
    0

Content Negotiation

Clients request RDF formats via the Accept header:

Accept Header Response Format
application/ld+json JSON-LD
text/turtle Turtle
application/rdf+xml RDF/XML
application/json (or any other) Original JSON (pass-through)

Semantic Toolchain

Use frank to extract an ontology from your F# types:

dotnet tool install --global frank
frank semantic extract --project MyApp.fsproj --base-uri https://example.org/api
frank semantic validate --project MyApp.fsproj
frank semantic compile --project MyApp.fsproj

The compiled artifacts are automatically embedded by Frank.Cli.MSBuild and loaded at startup by useLinkedData.

The CLI also provides unified commands for the full pipeline (semantic + statechart extraction):

frank extract --project MyApp.fsproj --base-uri https://example.org/api
frank generate --project MyApp.fsproj --format all --output ./specs
frank status --project MyApp.fsproj

See Spec Pipeline for the full CLI reference.


Frank.Datastar

Frank.Datastar provides seamless integration with Datastar, enabling reactive hypermedia applications using Server-Sent Events (SSE).

Version 7.1.0 features a native SSE implementation with zero external dependencies, delivering high-performance Server-Sent Events directly via ASP.NET Core's IBufferWriter<byte> API. Supports .NET 8.0, 9.0, and 10.0.

Installation

dotnet add package Frank.Datastar

Example

open Frank.Builder
open Frank.Datastar

let updates =
    resource "/updates" {
        name "Updates"

        datastar (fun ctx -> task {
            // SSE stream starts automatically
            do! Datastar.patchElements "<div id='status'>Loading...</div>" ctx
            do! Task.Delay(500)
            do! Datastar.patchElements "<div id='status'>Complete!</div>" ctx
        })
    }

// With explicit HTTP method
let submit =
    resource "/submit" {
        name "Submit"

        datastar HttpMethods.Post (fun ctx -> task {
            let! signals = Datastar.tryReadSignals<FormData> ctx
            match signals with
            | ValueSome data ->
                do! Datastar.patchElements $"<div id='result'>Received: {data.Name}</div>" ctx
            | ValueNone ->
                do! Datastar.patchElements "<div id='error'>Invalid data</div>" ctx
        })
    }

Available Operations

  • Datastar.patchElements - Update HTML elements in the DOM
  • Datastar.patchSignals - Update client-side signals
  • Datastar.removeElement - Remove elements by CSS selector
  • Datastar.executeScript - Execute JavaScript on the client
  • Datastar.tryReadSignals<'T> - Read and deserialize signals from request

Each operation also has a WithOptions variant for advanced customization.


Frank.Analyzers

Frank.Analyzers provides compile-time static analysis to catch common mistakes in Frank applications.

Installation

dotnet add package Frank.Analyzers

Available Analyzers

FRANK001: Duplicate HTTP Handler Detection

Detects when multiple handlers for the same HTTP method are defined on a single resource. Only the last handler would be used at runtime, so this is almost always a mistake.

// This will produce a warning:
resource "/example" {
    name "Example"
    get (fun ctx -> ctx.Response.WriteAsync("First"))   // Warning: FRANK001
    get (fun ctx -> ctx.Response.WriteAsync("Second"))  // This one takes effect
}

IDE Integration

Frank.Analyzers works with:

  • Ionide (VS Code)
  • Visual Studio with F# support
  • JetBrains Rider

Warnings appear inline as you type, helping catch issues before you even compile.


Building

Make sure the following requirements are installed in your system:

dotnet build

Contributing

After cloning, restore local tools and enable the pre-commit hook:

dotnet tool restore
git config core.hooksPath hooks

This installs Fantomas and enables a pre-commit hook that checks F# formatting. If a commit is blocked, format the staged files with:

git diff --cached --name-only --diff-filter=ACM | grep '\.fs$' | xargs dotnet fantomas

Sample Applications

The sample/ directory contains several example applications:

Sample Description
Sample Basic Frank application
Frank.OpenApi.Sample Product Catalog API demonstrating OpenAPI document generation
Frank.Datastar.Basic Datastar integration with minimal HTML
Frank.Datastar.Hox Datastar with Hox view engine
Frank.Datastar.Oxpecker Datastar with Oxpecker.ViewEngine
Frank.Falco Frank with Falco.Markup
Frank.Giraffe Frank with Giraffe.ViewEngine
Frank.Oxpecker Frank with Oxpecker.ViewEngine
Frank.LinkedData.Sample Linked Data content negotiation with semantic RDF responses
Frank.TicTacToe.Sample Stateful resource with affordance middleware, guards, and Datastar SSE

References


License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Frank.Statecharts.Core:

Package Downloads
Frank.Statecharts

Statechart-based resource state machine extensions for Frank web framework

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.3.0 35 3/25/2026

### New in 7.3.0

Self-describing, protocol-aware applications — legible to both human developers and machine agents, with formal multi-party session guarantees enforced at the HTTP boundary.

**Multi-Party Session Protocol Enforcement**

- **Role definitions** on stateful resources with typed guard projections
- **Projection operator** derives per-role ALPS profiles from the global statechart
- **Projected profile middleware** serves role-aware ALPS `Link` headers at zero per-request cost
- **Progress analysis** detects deadlock and starvation at build time
- **Projection consistency validator** with 4 MPST checks (safety, completeness, role independence, liveness)
- **Post-hoc session conformance checking** via `Frank.Provenance`
- **Role-aware SHACL shape references** for projected content negotiation and validation
- **`frank project` command** generates per-role ALPS profiles with `--base-uri` support

**Bidirectional Spec Pipeline**

- **Shared statechart AST** (`Frank.Statecharts.Core`) — unified type model for all format parsers
- **Cross-format validator** with Jaro-Winkler near-match detection across WSD, ALPS, SCXML, smcat, XState
- **Typed ALPS extension vocabulary** for role, guard, duality, and classification metadata
- **End-to-end extraction pipeline** with `loadOrExtract` caching and pure/impure separation
- **`frank extract`**, **`compile`**, **`validate`**, **`generate`**, **`diff`** CLI commands

**Discovery and JSON Home**

- **JSON Home document generation** at `GET /` via strict content negotiation
- **`useDiscovery`** as pit-of-success default — bundles OPTIONS, Link headers, and JSON Home
- **`useAffordances`** auto-loads pre-computed affordance map from embedded assembly resource
- **Combinatorial integration tests** for discovery middleware composition

**Frank.LinkedData — Semantic RDF Content Negotiation**

- **New library** for automatic RDF content negotiation (JSON-LD, Turtle, RDF/XML)
- **OWL ontology integration** projects JSON responses to RDF graphs using ontology-derived predicate URIs
- **`useLinkedData`** / **`useLinkedDataWith`** WebHostBuilder extensions

**Frank.Cli.MSBuild — Build-Time Artifact Embedding**

- **Content-only NuGet package** auto-embeds compiled semantic artifacts and affordance maps as assembly resources
- Works automatically via `buildTransitive/` targets

**Additional Improvements**

- **`IStatechartFeature`** typed feature replaces `HttpContext.Items` string conventions
- **Auto-infer `ResourceSpec.Name`** from route template
- **`frank` CLI** renamed from `frank-cli`; LLM-ready hierarchical help system
- **FRANK001 analyzer** extended to cover `datastar` operations
- **Constitution VII compliance** — all bare `with _ ->` catches replaced with logged helpers
- **Type design improvements** — `FoundStatefulResource` record carrier, `ArtifactKind` DU, `RoleProjectionResult` naming