Frank 7.2.1
dotnet add package Frank --version 7.2.1
NuGet\Install-Package Frank -Version 7.2.1
<PackageReference Include="Frank" Version="7.2.1" />
<PackageVersion Include="Frank" Version="7.2.1" />
<PackageReference Include="Frank" />
paket add Frank --version 7.2.1
#r "nuget: Frank, 7.2.1"
#:package Frank@7.2.1
#addin nuget:?package=Frank&version=7.2.1
#tool nuget:?package=Frank&version=7.2.1
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 unrepresentable. Resource behavior is constrained at the framework level so illegal interactions never appear in the response surface — not in headers, not in advertised affordances, not in rendered controls. The work shifts from defensive runtime checks to structural impossibility.
Built for the age of agents. Frank treats LLM-based and rule-based clients as first-class consumers. Semantic vocabularies, ALPS profiles, JSON Home directories, and Link relations describe what an application is and what it can do, so an agent that understands HTTP can navigate without an SDK or hand-written tool definitions.
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, and ALPS profiles define what things mean. 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
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.Auth ────────────────── Frank
│
├── Frank.OpenApi ─────────────── Frank
│
├── Frank.Datastar ────────────── Frank
│
└── Frank.Analyzers ──────────── (FSharp.Analyzers.SDK analyzer, no runtime dependency)
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.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
See CONTRIBUTING.md for setup, build instructions, design principles, and pull request guidelines.
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 |
References
- Design Documents — Design philosophy, vision, and architecture documents
- Semantic Resources Vision — Agent-legible applications and the self-describing app architecture
- Spec Pipeline — Bidirectional design spec pipeline (WSD, SCXML, ALPS)
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.301)
-
net8.0
- FSharp.Core (>= 10.1.301)
-
net9.0
- FSharp.Core (>= 10.1.301)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Frank:
| Package | Downloads |
|---|---|
|
Frank.Datastar
Datastar SSE integration for Frank web framework with F# computation expression support. Native SSE implementation with no external dependencies, supports .NET 8.0/9.0/10.0. |
|
|
Frank.Auth
Resource-level authorization extensions for Frank web framework |
|
|
Frank.OpenApi
OpenAPI document generation extensions for Frank web framework |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 7.2.1 | 162 | 6/22/2026 |
| 7.2.0 | 227 | 2/10/2026 |
| 7.1.0 | 147 | 2/8/2026 |
| 7.0.0-build.0 | 78 | 2/6/2026 |
| 6.5.0 | 193 | 2/5/2026 |
| 6.4.1 | 146 | 2/4/2026 |
| 6.4.1-build.0 | 78 | 2/4/2026 |
| 6.4.0 | 128 | 2/2/2026 |
| 6.4.0-build.0 | 75 | 2/2/2026 |
| 6.3.0 | 519 | 3/15/2025 |
| 6.2.0 | 5,503 | 11/18/2020 |
| 6.1.0 | 1,910 | 6/11/2020 |
| 6.0.0 | 937 | 6/2/2020 |
| 5.0.5 | 1,103 | 1/7/2019 |
| 5.0.4 | 1,052 | 1/6/2019 |
| 5.0.3 | 1,103 | 1/5/2019 |
### New in 7.2.1 (Released 2026-06-21)
**Frank.Datastar - Datastar v1 ADR Compliance**
- **`viewTransitionSelector` support:** `PatchElementsOptions.ViewTransition` is now a discriminated union (`NoViewTransition | ViewTransition of selector: string voption`). Set `ViewTransition(ValueSome "#my-el")` to emit both `useViewTransition true` and `viewTransitionSelector #my-el` in the SSE frame; `ViewTransition(ValueNone)` emits `useViewTransition true` without a selector. Aligned with the StarFederation.Datastar reference SDK.
- **DELETE signal routing:** `ReadSignalsAsync` and `ReadSignalsAsync<'T>` now read signals from the `datastar` query parameter for DELETE requests, matching the behaviour already in place for GET. Previously DELETE fell through to body parsing, returning empty signals.
- **`JsonException` surfacing:** `ReadSignalsAsync<'T>` now lets `JsonException` propagate instead of swallowing it. The `tryReadSignals` and `tryReadSignalsWithOptions` convenience wrappers catch `JsonException` and return `ValueNone`, logging a warning via `ILoggerFactory`. Call `ReadSignalsAsync<'T>` directly to handle parse errors yourself.
- **Thread-safety documentation:** `StartServerEventStreamAsync` now carries an XML doc note that `PipeWriter` is not thread-safe — writes to the same SSE stream must be serialised, which the `datastar` CE operation enforces implicitly via `task { }` linearisation.
- **Samples updated to Datastar JS v1.0.2:** All three Datastar sample apps (Basic, Hox, Oxpecker) now load the stable `v1.0.2` client script from the CDN (was `v1.0.0-RC.7`).