Frank.Alps 7.3.3

dotnet add package Frank.Alps --version 7.3.3
                    
NuGet\Install-Package Frank.Alps -Version 7.3.3
                    
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.Alps" Version="7.3.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Frank.Alps" Version="7.3.3" />
                    
Directory.Packages.props
<PackageReference Include="Frank.Alps" />
                    
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.Alps --version 7.3.3
                    
#r "nuget: Frank.Alps, 7.3.3"
                    
#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.Alps@7.3.3
                    
#: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.Alps&version=7.3.3
                    
Install as a Cake Addin
#tool nuget:?package=Frank.Alps&version=7.3.3
                    
Install as a Cake Tool

Frank.Alps

Frank.Alps serves ALPS (Application-Level Profile Semantics, draft-07) profile documents describing what your resources mean and which transitions are available. Profiles are hand-authored F# values — never derived from CLR types or view templates — and each transition is bound to the endpoint that implements it, so the served document is filtered per principal and, optionally, per resource state.

Features

  • Hand-authored profiles: plain |> combinators, or a descriptor { } computation expression — both produce identical Descriptor values
  • Compile-checked references: rt/href/from take a Descriptor value, not a string id, so a dangling reference is a compile error, not a wrong document
  • Composite-state authoring: contains/initial/regions express substate (OR) and orthogonal (AND) decomposition, riding ext so documents stay spec-valid for readers that don't know them
  • Zero-friction binding: binds descriptor inside handler { } attaches a transition to the endpoint that implements it — no separate registry to keep in sync
  • Startup validation: a transition's authored type is checked against its bound HTTP method; a mismatch fails host startup, and an unbound transition is logged as a warning, rather than either shipping silently
  • Authorization- and state-filtered documents: reads stock IAuthorizeData/AuthorizationPolicy metadata (no Frank.Auth dependency) at every depth of the profile, and optionally filters by a CurrentStateResolver you supply
  • Two HTTP exposures: an app-wide document at /.well-known/alps.json, and a per-resource excerpt wired into negotiate { }
  • Zero NuGet dependencies: only FrameworkReference Microsoft.AspNetCore.App and a project reference to Frank; no dependency on Frank.Auth, Frank.Rdf, or Frank.OpenApi

Installation

dotnet add package Frank.Alps

Quick Start

open Frank.Builder
open Frank.Alps

module Catalog =
    let openState = semantic "open" |> def "https://tictactoe.example/states/open"
    let closedState = semantic "closed" |> def "https://tictactoe.example/states/closed"

    let viewGame = safe "viewGame"
    let makeMove = unsafe "makeMove" |> from [ openState ] |> rt closedState

    // Transitions may be authored at the top level or nested under the state they act on.
    let game = semantic "game" |> doc "A tic-tac-toe game" |> contains [ viewGame; makeMove ]

webHost args {
    useDefaults

    resource "/games/{id}" {
        // Advertises the per-resource excerpt available at this same url.
        link (fun ctx ->
            Seq.singleton
                { Target = string ctx.Request.Path
                  Rel = "profile"
                  Params = [ "type", "application/alps+json" ] })

        get (
            negotiate {
                accepts "application/json" (handler {
                    handle getGameJson
                    binds Catalog.viewGame
                })

                accepts "application/alps+json" (Alps.excerpt None)
            }
        )

        post (handler {
            handle makeMoveHandler
            binds Catalog.makeMove
        })
    }

    useAlps [ Catalog.openState; Catalog.closedState; Catalog.game ]
}

A descriptor { } computation expression offers the same vocabulary as the |> combinators above, producing an identical Descriptor:

descriptor "makeMove" { unsafe; from [ Catalog.openState ]; rt Catalog.closedState }

API Reference

Authoring Operations

  • semantic / safe / unsafe / idempotent "id" — Constructs a descriptor of that type (semantic is the spec's default). Also available as zero-argument custom operations inside descriptor { }
  • doc "text" / docWith { Value; Href; Format; ContentType; Tag } — Human-readable documentation
  • def "iri" — The descriptor's source-definition IRI
  • named / rel / tagname, rel, and tag from draft-07 §2.2
  • ext "id" "value" / extWith { Id; Href; Value; Tag } — Author-specific extension data
  • link "href" "rel" / linkWith { Href; Rel; Title; Tag } — An RFC 8288 web link, distinct from descriptor inheritance
  • contains [ children ] — Nests descriptors (draft-07 §2.2.4), deliberately unrestricted by child type
  • rt target / href target / from [ sources ] — Descriptor-typed references: a dangling one is a compile error, not a wrong document. hrefExternal "uri" is the escape hatch for a document this codebase doesn't own
  • initial / regions [ children ] — Composite-state structure: the default child of a contains list, and orthogonal (AND) rather than substate (OR) decomposition. Both ride ext under https://frank-fs.github.io/alps-ext/, so documents stay spec-valid for readers that don't know them

Binding Transitions to Resources

binds descriptor, inside a handler { } block, records which transition an endpoint implements. That binding is what makes the rest work:

  • Startup validation: a transition's authored type is checked against its bound HTTP method (safe → GET/HEAD, idempotent → PUT/DELETE, unsafe → POST). A mismatch fails host startup rather than serving a wrong document. A transition in the profile that nothing binds is logged as a startup warning and omitted from the document.
  • Authorization filtering: reads stock IAuthorizeData/AuthorizationPolicy endpoint metadata, so it works with Frank.Auth without referencing it and equally with a plain AuthorizeAttribute. Evaluation failures deny. Filtering applies at every depth of the profile, so a guarded transition nested under a semantic state is hidden too. Semantic descriptors are never filtered — vocabulary, not capability.

Two HTTP Exposures

  • App-wide documentuseAlps [ ... ] serves the whole profile at /.well-known/alps.json (configurable) and advertises it with a Link: rel="profile" response header. Filtered by authorization only; there is no resource instance in scope to have a state.
  • Per-resource excerptAlps.excerpt resolver, wired into a negotiate { } block's accepts "application/alps+json" case, serves just the transitions bound to this resource's route (every HTTP method's, not only the one it runs under). Filtered by authorization and, when resolver is Some, by state. When a served (filtered) document references a descriptor absent from that filtered view, the reference resolves to the full document's URI (rootUri#id) rather than a dangling same-document fragment.

Both emit Cache-Control: private, no-cache and Vary: Authorization whenever any bound endpoint is guarded.

State-Based Filtering

from [ states ] marks a transition valid only from the given source state(s); a transition with no from is never state-filtered. CurrentStateResolver — a plain string -> Uri list, wired at composition time — answers "what states is this specific resource concurrently in" (one element per active orthogonal region):

let resolver: CurrentStateResolver =
    fun resourceIri -> if isFinished resourceIri then [ closedIri ] else [ openIri ]

accepts "application/alps+json" (Alps.excerpt (Some resolver))

No dependency on any store: the natural implementation queries a provenance or event store, and an absent resolver (or one returning []) simply means state filtering does not apply. Matching walks contains ancestry rather than requiring exact equality, so being in a substate satisfies a transition declared from any of its ancestors. A transition's from candidate is satisfied if any element of the resolved list satisfies it (existential/OR match across concurrently-active regions) — conjunctive AND-guards and multi-region fan-out targets are out of scope here (frank-fs/frank#489).

ProtocolGraph.ofProfile derives the read-only { FromGuard: StateGuard option; Transition: Descriptor; ToTargets: TransitionTarget list } edge set from the authored profile. FromGuard comes from Guard (set via guardedBy) if present, else from From: empty yields None (unconditional — the edge fires regardless of prior state), one state yields Some (State s), and multiple states now collapse into a single Some (Any [State s1; State s2; ...]) edge rather than one edge per source state as before. ToTargets comes from Targets (set via entersRegions) if non-empty, else from Rt: Some yields one [EnterState t], None yields []. An edge is emitted iff the resulting ToTargets is non-empty — FromGuard is independently optional, so a transition declaring only rt (no from/guardedBy) now yields an unconditional edge, where previously it was excluded. Nothing in this package executes a transition or owns what state a resource is actually in. See docs/superpowers/specs/2026-08-08-frank-alps-compound-transitions-design.md for the full design rationale behind this shape and both behavior changes.

Architecture

Frank.Alps is built on:

  1. Frank: Provides the computation expression framework for defining HTTP resources and the HandlerDefinition/endpoint-metadata mechanism binds writes into
  2. ASP.NET Core: Endpoint/EndpointDataSource are read directly (EndpointSurface) rather than through an ApiExplorer dependency

There is no dependency on Frank.Rdf, Frank.JsonHome, Frank.Provenance, or Frank.AuthAuthorizationFilter reads stock ASP.NET Core authorization metadata, so it composes with Frank.Auth without referencing it.

See sample/Frank.Alps.Sample for a runnable demonstration of both HTTP exposures and both Link headers.

  • Frank - F# web framework
  • ALPS - Application-Level Profile Semantics (draft-07)
  • RFC 6906 - The 'profile' Link Relation Type

License

MIT License - see LICENSE file for details

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.3.3 92 8/11/2026
7.3.2 100 8/10/2026

### New in 7.3.3 (Released 2026-08-10)
**Frank.Rdf - Async IBufferWriter Streaming**
- **New: `Doc.writeJsonLdAsync doc bufferWriter`** — async overload that writes JSON-LD expanded-form directly to an `IBufferWriter<byte>` (e.g. `HttpResponse.BodyWriter`/`PipeWriter`), without intermediate string allocation or copying. Encodes UTF8 directly to the buffer for maximum efficiency in response streaming. Completes after serialization and flushing to the buffer.
- **Recommended for response streaming:** `writeJsonLdAsync` is the preferred method when serving JSON-LD from a Frank handler over HTTP. Streaming directly to `PipeWriter` avoids `AllowSynchronousIO` requirements and eliminates intermediate buffering layers that `StreamWriter` would introduce.
- **Three serialization options now available:** Use `writeJsonLdAsync` for HTTP responses (most efficient), `writeJsonLd` for flexibility with any `TextWriter`, and `toJsonLd` for testing/debugging.
- **Sample updated:** `sample/Frank.Rdf.Sample` demonstrates `Doc.writeJsonLdAsync` streaming the `application/ld+json` representation directly to the response body via `negotiate { }` content negotiation.
- **Test coverage:** comprehensive test cases for `writeJsonLdAsync` including single and multi-subject documents, language-tagged strings, and round-trip parsing.