F# computation expressions for configuring the Microsoft.AspNetCore.Hosting.IWebHostBuilder and defining routes for HTTP resources using Microsoft.AspNetCore.Routing.
$ dotnet add package FrankF# computation expressions, or builders, for configuring the Microsoft.AspNetCore.Hosting.IWebHostBuilder and defining routes for HTTP resources using Microsoft.AspNetCore.Routing.
This project was inspired by @filipw's Building Microservices with ASP.NET Core (without MVC).
WebHostBuilder - computation expression for configuring WebHostResourceBuilder - computation expression for configuring resources (routing)Builder with your own methods!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
Frank provides two middleware operations with different positions in the ASP.NET Core pipeline:
Request → plugBeforeRouting → UseRouting → plug → Endpoints → Response
plugBeforeRoutingUse for middleware that must run before routing decisions are made:
webHost args {
plugBeforeRouting HttpsPolicyBuilderExtensions.UseHttpsRedirection
plugBeforeRouting StaticFileExtensions.UseStaticFiles
resource myResource
}
plugUse for middleware that needs routing information (e.g., the matched endpoint):
webHost args {
plug AuthenticationBuilderExtensions.UseAuthentication
plug AuthorizationAppBuilderExtensions.UseAuthorization
resource protectedResource
}
Both plugWhen and plugWhenNot run in the plug position (after routing):
webHost args {
plugWhen isDevelopment DeveloperExceptionPageExtensions.UseDeveloperExceptionPage
plugWhenNot isDevelopment HstsBuilderExtensions.UseHsts
resource myResource
}
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 provides resource-level authorization for Frank applications, integrating with ASP.NET Core's built-in authorization infrastructure.
dotnet add package Frank.Auth
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"))
}
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
| 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 provides native OpenAPI document generation for Frank applications, with first-class support for F# types and declarative metadata using computation expressions.
dotnet add package Frank.OpenApi
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
}
| 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, Async<'a>) |
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>
}
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.
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 { (* ... *) })
}
Frank.OpenApi is fully backward compatible with existing Frank applications. You can:
HandlerDefinition and plain RequestDelegate handlers in the same resourceFrank.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.
dotnet add package Frank.Datastar
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
})
}
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 requestEach operation also has a WithOptions variant for advanced customization.
Frank.Analyzers provides compile-time static analysis to catch common mistakes in Frank applications.
dotnet add package Frank.Analyzers
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
}
Frank.Analyzers works with:
Warnings appear inline as you type, helping catch issues before you even compile.
Make sure the following requirements are installed in your system:
dotnet build
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 |