Frank.Validation 7.3.3

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

Frank.Validation

Hand-authored SHACL Core (+ SPARQL-based constraints, + the full property-path grammar) validation for Frank resources, built on Frank.Rdf.

Authoring a shape

open Frank.Validation
open Frank.Validation.ShapeSpecFunctions

let personShape =
    shape (targetClass (Uri "https://schema.org/Person")) {
        properties [
            property (PropertyPath.Predicate(Uri "https://schema.org/email")) {
                datatype XsdDatatype.String
                pattern @"^\S+@\S+\.\S+$"
                minCount 1
            }
        ]
        closed []
    }

shape { }/property { } are optional sugar over ShapeSpecFunctions -- both produce identical ShapeDecl/PropertyShapeSpec values; use whichever reads better at the call site.

Validating a graph

let shapesGraph = Shacl.toShapesGraph [ personShape ]

match Shacl.validate shapesGraph someDataGraph with
| ValidationOutcome.Conforms -> ()
| ValidationOutcome.Violates violations -> (* ... *)

Validating HTTP request bodies

resource "/people" {
    useValidation shapesGraph
    post createPerson
}

webHost args {
    useDefaults
    useValidation   // registers the one app-wide interceptor -- required once, app-wide
    resource peopleResource
}

POST/PUT/PATCH requests with Content-Type: application/ld+json to a useValidation-declared resource are buffered, parsed, and validated before the handler runs. A conforming request continues to the handler unchanged; a violating request gets 422, content-negotiated between a real sh:ValidationReport (Accept: application/ld+json) and application/problem+json (everything else).

What is, and is not, intercepted

A request is validated only when all of the following hold. Anything else passes straight through to the handler, unvalidated:

Intercepted
Resource declared useValidation shapesGraph on its resource { }
App called useValidation once on its webHost { } (without this, nothing is validated)
Method POST, PUT or PATCH — never GET/DELETE/HEAD/OPTIONS
Content-Type starts with application/ld+json (case-insensitive), so application/ld+json; charset=utf-8 counts

The Content-Type check is a real bypass, not a formality. A client that sends the very same body as application/json, or with no Content-Type header at all, to a useValidation-declared resource reaches your handler unvalidated. This is deliberate — the middleware only ever narrows JSON-LD, and it must not reinterpret a payload whose media type says it is something else — but it means useValidation is not a guarantee that every request your handler sees has been SHACL-checked.

A handler that must not run on unvalidated input is responsible for its own defence: reject unexpected content types itself (a 415 Unsupported Media Type is the usual answer), or treat Validation.tryGetValidatedGraph ctx returning None as "this body was never validated" and act accordingly.

Other short-circuit responses, all application/problem+json:

Condition Status
Body over the 1 MiB buffering limit 413
Body is not parseable JSON-LD 400 (a parse failure is not a SHACL violation)
SHACL violation 422 (or a sh:ValidationReport under Accept: application/ld+json)
Unexpected failure inside the RDF/SHACL layer 500, logged

Reading the parsed graph in a handler

A conforming request's parsed graph is stashed for the handler, so it never has to parse the body a second time:

let postMove =
    fun (ctx: HttpContext) ->
        task {
            match Validation.tryGetValidatedGraph ctx with
            | Some graph -> // the graph the middleware already validated
            | None -> // this request was not validated -- see the bypass note above
        }

An empty body parses to an empty graph and conforms trivially, unless a shape targets via TargetSpec.Node.

A note on SPARQL constraints

sh:sparql takes a SPARQL SELECT query, and every row it returns is one violation — so write the query to select what is wrong. Shacl.toShapesGraph parses each query at shape-build time and raises there if it does not parse, or is not a SELECT, rather than letting a shape bug fail every request to the resource it guards. An ASK { P } inverts to SELECT $this WHERE { FILTER NOT EXISTS { P } }.

See sample/Frank.Validation.Sample for a complete, runnable example.

Non-goals

SHACL-JS, non-validating shape characteristics (sh:name/sh:order/...), and durable shape storage are explicitly out of scope -- see docs/superpowers/specs/2026-08-03-frank-validation-design.md.

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 90 8/11/2026
7.3.2 86 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.