A Flurry of Library Updates: FSharp.Data.JsonSchema and Frank

I recently carved out some time to revisit some dormant projects. The primary driver was a tic-tac-toe app inspired by Scott Wlaschin‘s Enterprise Tic-Tac-Toe series of posts and presentation and a desire to learn and test out Datastar. An upcoming post will dive deeper into that topic. In this post, I want to share a high-level overview of what’s new with FSharp.Data.JsonSchema and Frank. Subsequent posts will dive into further details.

FSharp.Data.JsonSchema 3.0.1

FSharp.Data.JsonSchema is now three packages:

  • FSharp.Data.JsonSchema.Core: Core JSON Schema representation types that relies on only FSharp.SystemTextJson. This library can be uses to parse a JSON Schema into F# types and F# types into a JSON Schema. However, it no longer has a specific target.
  • FSharp.Data.JsonSchema.NJsonSchema: This is equivalent to the previous version with a dependency on NJsonSchema as the target. This should be backward compatible with previous versions of the library.
  • FSharp.Data.JsonSchema.OpenApi: The new target depends on the Microsoft.OpenApi library introduced with the net9.0 framework target. This target is intended for use with generating Open API documents from ASP.NET Core applications.

In addition, several long overdue bug fixes and enhancements should now be resolved:

  • Recursive Types (#15): Recursive F# types no longer cause infinite loops. Self-referential DUs, records with optional self-references, and recursion through collections all generate proper $ref: "#" schemas, and a follow-up fix in 3.0.1 resolved an NJsonSchema serialization failure where Ref("#") in nullable contexts tried to look up the root reference in the definitions dictionary instead of referencing the root schema directly
  • Choice types (#22): Choice<'A,'B> through Choice<'A,…,'G> now generate clean anyOf schemas instead of the verbose internal-tag encoding
  • Anonymous records: Inline object schemas, no $ref
  • DU encoding styles: InternalTag, AdjacentTag, ExternalTag, and Untagged via a new unionEncoding parameter on Generator.Create
  • Format annotations: Proper date-time, guid, uri, duration, date, time, and byte formats for DateTime, Guid, Uri, TimeSpan, DateOnly, TimeOnly, and byte[]

Frank 7.2.0

Frank has had a long and winding history as my favorite hobby project for trying out different approaches to encoding web applications. The computation expression approach starting in (IIRC) v5.0 has stuck. The goal is still to produce an HTTP resource-style set of builders that provides a consistent means of defining HTTP resources and the ASP.NET Core WebHost in which to run them while allowing for a lot of flexibility and extensibility. As such, I’ve added some additional libraries I’ve found useful to test out the extensibility and support the new tic-tac-toe hobby project mentioned above.

Packages

  • Frank: Added Metadata field to ResourceSpec, a list of (EndpointBuilder -> unit) convention functions applied during RouteEndpointBuilder.Build(). This generic extensibility point lets companion libraries (Auth, OpenApi, etc.) attach typed endpoint metadata without requiring changes to the core Frank library. This is a binary-breaking change but source-compatible with the empty default. Also added plugBeforeRouting, plugBeforeRoutingWhen, and plugBeforeRoutingWhenNot for middleware ordering control around UseRouting().
  • Frank.Analyzers: F# Analyzer (FSharp.Analyzers.SDK) that detects duplicate HTTP handler registrations within a resource block at compile time, enforcing the constraint of a single HTTP method per resource. It works in IDEs (Ionide, VS, Rider) and CLI (dotnet fsharp-analyzers) for CI/CD.
  • Frank.Auth: Adds WebHostBuilder registration and resource-level authorization via ResourceBuilder extensions, including requireAuth, requireClaim, requireRole, requirePolicy using AND semantics for resources and useAuthentication, useAuthorization, authorizationPolicy for WebHostBuilder.
  • Frank.OpenApi: Adds long-planned, declarative OpenAPI 3.0+ document generation, including a handler computation expression for pairing handlers with metadata (name, summary, tags, produces, accepts). F# type schemas (records, DUs, options, collections) via FSharp.Data.JsonSchema.OpenApi. useOpenApi on WebHostBuilder wires services and middleware. Includes Scalar UI to provide a web-based client for viewing and testing endpoints. Targets net9.0/net10.0.
  • Frank.Datastar: Native SSE implementation similar to the StarFederation.Datastar.FSharp library. Zero-copy buffer writing via IBufferWriter, zero external NuGet dependencies, full Datastar SDK ADR compliance. Added stream-based overloads (streamPatchElements, etc.) accepting TextWriter -> Task for zero-allocation HTML rendering. No breaking API changes. Targets net8.0/net9.0/net10.0.

New Samples

  • Frank.Datastar.Basic: RESTful hypermedia patterns (click-to-edit, search, bulk ops) using Frank.Datastar with F# string templates.
  • Frank.Datastar.Hox: Same patterns as Basic using the Hox view engine. Demonstrates stream-based SSE overloads via Render.toStream.
  • Frank.Datastar.Oxpecker: Same patterns as Basic using Oxpecker.ViewEngine.
  • Frank.OpenApi.Sample: Product catalog API demonstrating the handler CE with OpenAPI metadata, mixed plain/enriched handlers, useOpenApi, and Scalar UI.

Looking Ahead

I’ll spend some time in upcoming posts exploring each of these. In the meantime, I’d love feedback on any of the updates and changes. I hope I haven’t broken anyone with changes to FSharp.Data.JsonSchema, and there is a transition package with version 3.0.0 to make it easier to switch without changing package names. I don’t have any additional plans at the moment for either of these, but please open issues if you have ideas or bug reports.

Video

F# on the Web

I recently presented to the Houston Functional Programmers meetup on the topic of using F# for web development. This is an update to my talks with a similar title based on my experience building a real application for Tachyus. I cover data access using the FSharp.Data.SqlClient and building web APIs using ASP.NET Web API with Frank. You can find the video on YouTube.

The Frank application signature is currently undergoing a…

The Frank application signature is currently undergoing a change from 'Request -> Async<'Response> to 'Request -> 'Response, where the 'Response includes a Body type that is:

type Body =
| Bytes of byte[]
| AsyncBytes of Async<byte[]>
| Segment of ArraySegment<byte>
| AsyncSegment of Async<ArraySegment<byte>>
| Str of string
| AsyncStr of Async<string>
| Sequence of seq<Body>
| AsyncSequence of AsyncSeq<Body>
view raw FrankBody.fsx hosted with ❤ by GitHub

Now to your questions:

Q: Why are you getting rid of the async processing?
A: I’m not. Most requests have already pulled in all the data required for processing, so wrapping the entire transaction in an Async needlessly is just waste. The Body of the 'Response can now be either Async or not, which can further wrap asynchronous access to the 'Request body.

Here’s another question related to Frank What should…

Here’s another question related to Frank: What should I do about converting existing platforms’ request and response types? I had originally planned on creating my own Request and Response types, but the cost to convert back and forth may or may not be worth it. I either have to create the conversions or a different set of operators per platform.

Functional programming really lends itself to the either, but the latter is a bit more exciting. Why? Take a look at Mauricio’s latest post on Figment. He’s refactored to the Reader monad. Let’s say Frank uses the Reader and Writer monads as a public API for managing web applications. I could then allow developers to use Figment as their implementation on top of ASP.NET and build additional adapters for WebSharper, WCF, OWIN, etc. Frank would then provide a simple, functional, DSL for routing and composing web applications, even using components built on different platforms… which has been the goal all along.

The best benefit, imho, is that developers already familiar with various request/response abstractions can continue using those abstractions. I don’t provide you with yet another HTTP abstraction. This is a significant departure from all other web frameworks of which I know. I suppose the benefit is balanced against the ease of building the adapter using the monad. Time to put functional programming to the test. I am quite certain that functional programming will win.

I’ve been working on the Frank syntax lately…

I’ve been working on the Frank syntax lately. Following some help from @talljoe, Frank maps paths from a resource level rather than each HTTP method. Frank can then take advantage of combinators that add HEAD and OPTIONS based on the HTTP methods explicitly supported by the developer … something missing from most web frameworks, the notable exception being OpenRasta.

Below are samples of two approaches: function and resource. Both are quite noisy at the moment, but I plan to continue reducing the footprint.

let echoAgent =
frank "/" [
get <| fun _ -> OK (Str "Hello, world!")
post <| fun request ->
(fun input -> OK (Sequence [ for x, y in input -> Str (x + "=" + y) ]))
<!> parseFormUrlEncoded request
] |> Extend.withOptions
view raw EchoAgent.fs hosted with ❤ by GitHub
let echoAgent =
FrankResource("/",
[
get <| fun _ -> OK (Str "Hello, world!")
post <| fun request ->
(fun input -> OK (Sequence [ for x, y in input -> Str (x + "=" + y) ]))
<!> parseFormUrlEncoded request
])
view raw EchoResource.fs hosted with ❤ by GitHub

Thoughts?

I haven’t had a lot of time recently…

I haven’t had a lot of time recently to do much with OWIN, WCF Web API, Frack, Frank, or any of the projects I’ve been working on lately. I’m staring to pick back up on Frack, which will be undergoing some heavy refactoring, mostly for performance.

I’m also going to be finishing out the HTTP parser (finally). The big hold up so far has been trying to figure out how to do it in pieces and keep track of entire messages. Instead, I’m just going to go the old-fashioned route of expecting a complete message and parsing from beginning to end. (I should have started there.)

Frank will get a complete rewrite. Why? At this point, I’ve almost completely forgotten how or why I did some things. That’s never a good sign. It’s also a lot more complex than I was ever intending. I might arrive at some similarities, but I think a rewrite will serve it well.

In the meantime, I’ve been working on porting some sites to WebSharper. I love it. The only things I’m trying to work out are how to pull in markdown and .fsx files to render content for a lightweight git-based cms. I’m also trying to see how this might run on top of Frack.

I’m off to Boise Code Camp this weekend…

I’m off to Boise Code Camp this weekend to present on F# on the Web. No slides, just code. I’ll be talking about Frack, Frank, and WebSharper, as well as noting Dave Thomas’ F# High Performance Sockets library.

In case you are interested, Frack is available as a download from CodeBetter’s TeamCity site. Frank will be joining shortly.

Another OWIN call is scheduled for this weekend, at which point I’m likely to pick up that thread again or just move on to discussing Frack and Frank.

A New Web for .NET

A number of things have been happening these last few years in the .NET community in relation to the Web. Specifically, OpenRasta and FubuMvc demonstrated 1) new approaches to web development in a static-typed world and 2) that other developers were growing tired of the existing options. Since then a host of new micro-frameworks, generally inspired by the Ruby dynamic duo of Rack and Sinatra, have popped up on github. In addition, several new server options have begun cropping up, most notably Kayak and Manos de Mono, both of which use an event loop a la node.js and primarily targeting Mono.

Microsoft has not be sitting idly by either. The WCF team is working on a new Web API to provide WCF developers simpler, more direct control over HTTP services. This is no slouch effort either. In fact, aside from OpenRasta, it may be the most thorough HTTP implementation available.

While exciting on their own, the best news, imho, is the .NET HTTP Abstractions group, started by Scott Koon. This group has been working on a standard, currently called Open Web Interface for .NET, or OWIN. It’s intent is to define a common interface by which any application can talk to any server. The idea comes from the Web Server Gateway Interface (Python) and Rack. The primary difference in this and other similar specs is the inclusion of asynchronous network I/O as a primary concern. Discussions are still underway and include nearly all of the developers of the aforementioned projects, as well as some members of the ASP.NET team.

If you are a F# fanboy, such as myself, you will be happy to know that F# is not silent in this space. WebSharper is now in its second iteration, and my own projects, Frack and Frank, are making nice headway. Frack is now a web server, similar in spirit to node.js. Frank is, as the name implies, a Sinatra-inspired clone that takes cues from the Haskell Snap framework. If you are interested in parsing and agent-based development, you’ll find more examples of how F# handles these things as these two projects progress.

Expect to find more posts (shock!) coming soon describing in more detail the WCF Web APIs, OWIN, Frack, Frank, and more. In the meantime, join the discussion on the .NET HTTP Abstractions group!

A Diversion

While you wait on real content, which I promise really is coming, I’m hoping I can divert your attention to some really cool goings-on:

  1. IronRuby 1.0 was released!
  2. REST in Practice is finished and is going into publication this year!
  3. Fantastic Herding Code episode!

If you pay close attention, you’ll notice a familiar name scattered throughout. In addition to some of those things, I’ve been doing a lot of projects as opposed to writing, hence the lack of new content here. If you are curious, you might check out these:

  1. FSharp.Monad (a lot of this is from Matt Podwysocki)
  2. FSharp.Reactive (this is mostly from Steffen Forkmann)
  3. Statefulie
  4. Frank
  5. Etc for IronRuby
  6. DataMapper for IronRuby (no repo yet)
  7. FAKE (helping add mono support)
  8. NaturalSpec (helping add mono support)

Hopefully, by the time you finish checking all that out in depth, I’ll have real, new content. 🙂