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
<PackageReference Include="Frank.Statecharts.Core" Version="7.3.0" />
<PackageVersion Include="Frank.Statecharts.Core" Version="7.3.0" />
<PackageReference Include="Frank.Statecharts.Core" />
paket add Frank.Statecharts.Core --version 7.3.0
#r "nuget: Frank.Statecharts.Core, 7.3.0"
#:package Frank.Statecharts.Core@7.3.0
#addin nuget:?package=Frank.Statecharts.Core&version=7.3.0
#tool nuget:?package=Frank.Statecharts.Core&version=7.3.0
Frank
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 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 configuringWebHostResourceBuilder- 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
Builderwith 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
HandlerDefinitionand plainRequestDelegatehandlers 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 DOMDatastar.patchSignals- Update client-side signalsDatastar.removeElement- Remove elements by CSS selectorDatastar.executeScript- Execute JavaScript on the clientDatastar.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 SDK 8.0 or higher
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
- Design Documents — Design philosophy, vision, and architecture documents
- Frank.Statecharts Guide — Core concepts, hierarchical statechart support, guards, and test coverage overview
- Semantic Resources Vision — Agent-legible applications and the self-describing app architecture
- Spec Pipeline — Bidirectional design spec pipeline (WSD, SCXML, ALPS)
- How is this different from Webmachine or Freya? — Detailed comparison of Frank.Statecharts with Webmachine and Freya's approach to HTTP resource state machines
License
| Product | Versions 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. |
-
net10.0
- FSharp.Core (>= 10.1.201)
-
net8.0
- FSharp.Core (>= 10.1.201)
-
net9.0
- FSharp.Core (>= 10.1.201)
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