OpenAPI and AsyncAPI generation for 5.3.0, with #913 and #914 - #915
Merged
Conversation
The generation work that follows needs two things the document primitives did not offer. A document has to be able to say which workspace created it, so a caller can hand a value to something that owns a lifetime rather than guessing whether one is shared. And a builder has to be able to hand its rows to another document without a serialize-and-reparse round trip. JsonDocumentBuilder now implements IWorkspaceCreatedDocument, and the document interface gains the two operations that make the handover possible: reading a local element's contiguous UTF-8 without materialising it, and appending a local element's rows into another metadata database rebased to their new location. Both are additive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Four changes to what the OpenAPI generators emit. A generated client can now take a context-threaded request body. The server result factory has long offered Ok<TContext>(Source<TContext>, workspace), so a caller assembles a body lazily with its context threaded through and it materialises in one pass with no per-item closure. A client had no counterpart, so anyone with a collection to put in a REQUEST body had to close over it. The machinery was already there: the generators take the set of body pointers whose type is an object or array, and emit the generic overload only for those. The server command computed that set and the client command never did, so the client path silently opted out under what its own doc comment called the conservative default. OpenAPI 2.0 was worse and is worth naming: the parameter did not exist there at all, so a 2.0 SERVER was also missing the closure-free response factories every 3.x server has had. A binary response now carries its body through the result factory, which is a breaking change. The old parameterless Ok() could not express a body at all; the shipped example recipe said so in a comment and returned Ok() anyway. It now takes the bytes or a writer, and the content type the handler chooses. An optional request body is optional. A body not marked required generated a mandatory parameter, so a caller had to supply something for a body the specification says may be absent. Descriptions in the source document become XML doc comments on the generated members, escaped so a description containing markup does not break the build. The schema classifier takes the document root alongside the schema so it can follow a reference rather than classifying the reference itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Request/reply had only one half. A caller could send a request and await the correlated reply, but nothing in the transport surface let a service BE the responder: subscribe to a channel, handle each request, and publish the reply on the correlated reply channel. SubscribeReplyAsync is that half. It ships with a default implementation that throws NotSupportedException, so a transport that does not support responders is unaffected and an existing custom transport still compiles. RequestAsync now takes the JsonWorkspace that owns the reply's lifetime, which is a breaking change: it is a required parameter ahead of the optional headers and cancellation token, so every call site and every custom implementation of the abstract overload needs it. The reply was previously materialised against a lifetime the caller could not control, which is the wrong shape for a caller folding the reply into a document it owns. The generated methods for a parameterised channel address now take a span or UTF-8 memory as well as a string, so an address composed from bytes does not have to become a string on the way to the transport. The producer's byte overload rents a pooled buffer, because the send outlives the call that started it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
AsyncAPI has had a Polly package since its transports did: a resilience pipeline wrapped around the transport, so a deployment configures retry and circuit-breaking without every call site knowing about it. OpenAPI had the same need and no counterpart, so a caller wanting a resilient transport wrapped IApiTransport by hand. ResilientApiTransport is that counterpart, decorating an IApiTransport with a Polly pipeline and passing every operation through unchanged otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Mechanical, with one exception. The recipes carry committed generator output, so a generator change leaves them stale until they are regenerated. The exception is the advanced-server recipe, whose download handler returned the parameterless Ok() a binary response used to generate, with a comment saying the streaming was handled elsewhere and a discarded local standing in for the photo it could not send. The factory now takes the body, so the recipe sends the photo and its content type, which is what it was describing all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
The prose that goes with the preceding commits: the AsyncAPI guide gains the responder and the workspace-carrying request, the OpenAPI guide gains the context-threaded request body, and two new documents cover consuming generated types and the performance techniques the generators now rely on. The agent guidance moves with it. Three new skills (context threading, the bytes-to-bytes discipline, typed model construction) and two updated ones describe the conventions these generators emit against, which is exactly the guidance a contributor needs to extend them without reintroducing the closures and round trips this work removed. 5.3.0 rather than 5.2.14 because two changes are breaking: RequestAsync takes a workspace, and a binary response carries its body through the result factory. This repository has shipped breaking changes in a patch before (5.2.7 renamed a generated member for schemas with a property called "create"), but that one reached almost nobody and only on regeneration. These reach every caller of AsyncAPI request/reply and every handler returning a binary response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Merges 9ddc809 by Levy Barbosa (@Levyks), from #913 A consumer whose channel or operation declares bindings is now subscribed with them, as a MessageContext, so a transport receives the protocol-specific metadata the specification carried instead of it stopping at the generator. The conflict was structural. The contribution emitted the context construction twice, once in each of the duplicated StartAsync bodies it was written against; this branch had already collapsed those into a single emission point. The resolution puts the logic there instead, so it is written once and covers both the asynchronous and the synchronous form. It also reaches the responder form, which the contribution could not have covered because SubscribeReplyAsync did not exist when it was written. That needed a MessageContext overload on SubscribeReplyAsync to receive it, added alongside with a default implementation that drops the context and forwards, so no existing transport has to change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
…tion-free Merges 8b58bc8 by Levy Barbosa (@Levyks), from #914 A consumer whose channel address declares parameters now gets them as method arguments on StartAsync, and composes the address from the template the specification declared. Conflicts were structural rather than semantic. This branch had already refactored the two duplicated StartAsync bodies into one, and lifted the subscribe path into a private Core that receives the retained UTF-8 bytes, so the contribution's inline edits landed on a shape that no longer existed. The resolution keeps this branch's structure and adds a runtime-address form beside the dynamic one: both public surfaces differ, and both end at the same Core. The composition is allocation-free, which the original could not be through no fault of its own: it was written against the string-based shape that was there. Composing by string.Replace costs a string per parameter, plus the joined result, plus a second string to concatenate the dead-letter prefix, and then transcodes all of it. The template is now split at generation time, so its literal parts are u8 literals that cost nothing at runtime and only the parameter values are transcoded, filled once into the array the subscription retains. A ReadOnlySpan<char> overload sits under the string one, so a caller holding a span never makes a string just to have it measured and copied. The dead-letter address is built from the bytes just composed rather than from a second pass. Two assertions guard the shape: the generated code must contain no channel template constant and no string.Replace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
The AsyncAPI consumer changes reach the committed recipe output, and the
streetlights recipes show why the parameterised-channel contribution matters:
they subscribed to
smartylighting.streetlights.1.0.action.{streetlightId}.lighting.measured
literally, placeholder and all, so the generated consumer listened on a channel
no publisher ever wrote to. They now take the streetlight id and compose the
address from it.
VERSIONHISTORY credits both contributions to Levy Barbosa, and records that the
allocation-free composition was added when #914 was merged rather than being
part of what was contributed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
The parameterised-channel work broke twenty-one call sites in the AsyncAPI
runtime tests, and that is the feature rather than a regression. Those tests
were asserting the bug: they delivered to
smartylighting.streetlights.1.0.action.{streetlightId}.lighting.measured
with the placeholder still in it, because that is the address the generated
consumer really subscribed to. They now pass a streetlight id and deliver to
the address it composes.
The release notes move that change from New features to Breaking changes. It is
both, but the half a consumer has to act on is the compile break: StartAsync()
compiled before and does not now. Twenty-one call sites in one test project is
a fair preview of what a downstream consumer meets.
They also record that this reaches AsyncAPI 2.6, not only 3.0. The 2.6
generator holds a 3.0 generator and delegates emission to it, so both
contributions applied to 2.6 the moment they applied to 3.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Contributor
Three gaps, found by asking what a reader would have to reverse-engineer from the release notes. docs/OpenApi.md had not been touched at all, so the headline client feature and a breaking change were both undocumented. It now covers building a request body without closures (the client counterpart of the server's Ok<TContext>), that an optional body is optional, and that a binary response carries its body through the result factory. docs/AsyncApi.md documented channel parameters for producers only, which was correct until now: consumers ignored them and subscribed to the template literally. The section now covers the consumer side, including the span overload, and says plainly what the old behaviour was, since anyone upgrading meets the compile break first and deserves to know it was hiding a bug. Corvus.Text.Json.OpenApi.Polly shipped with no documentation whatsoever. It now has the section its AsyncAPI counterpart has had, with the caveat that a retry pipeline cannot tell an idempotent operation from one that is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq
Contributor
|
Contributor
Code Coverage Summary Report - Linux (net10.0)Summary
CoverageCorvus.Json.CodeGeneration - 80.5%
Corvus.Json.CodeGeneration.CSharp - 81.8%
Corvus.Json.ExtendedTypes - 74.9%
Corvus.Json.JsonReference - 76.7%
Corvus.Text.Json - 94.1%
Corvus.Text.Json.AsyncApi - 62%
Corvus.Text.Json.AsyncApi.Amqp - 0.3%
Corvus.Text.Json.AsyncApi.AzureServiceBus - 0%
Corvus.Text.Json.AsyncApi.CodeGeneration - 93.3%
Corvus.Text.Json.AsyncApi.Kafka - 0.3%
Corvus.Text.Json.AsyncApi.Mqtt - 0.5%
Corvus.Text.Json.AsyncApi.Nats - 0.3%
Corvus.Text.Json.AsyncApi.Polly - 100%
Corvus.Text.Json.AsyncApi.Testing - 96%
Corvus.Text.Json.AsyncApi.WebSocket - 0%
Corvus.Text.Json.CodeGeneration - 93.2%
Corvus.Text.Json.JMESPath - 93.8%
Corvus.Text.Json.JMESPath.CodeGeneration - 97.7%
Corvus.Text.Json.Jsonata - 91.6%
Corvus.Text.Json.Jsonata.CodeGeneration - 88.5%
Corvus.Text.Json.JsonLogic - 95.1%
Corvus.Text.Json.JsonLogic.CodeGeneration - 92.8%
Corvus.Text.Json.JsonPath - 94.7%
Corvus.Text.Json.JsonPath.CodeGeneration - 96.1%
Corvus.Text.Json.OpenApi - 86.7%
|
Contributor
Code Coverage Summary Report - Windows (net10.0)Summary
CoverageCorvus.Json.CodeGeneration - 80.5%
Corvus.Json.CodeGeneration.CSharp - 81.8%
Corvus.Json.ExtendedTypes - 74.9%
Corvus.Json.JsonReference - 76.7%
Corvus.Text.Json - 93.9%
Corvus.Text.Json.AsyncApi - 62%
Corvus.Text.Json.AsyncApi.Amqp - 0.3%
Corvus.Text.Json.AsyncApi.AzureServiceBus - 0%
Corvus.Text.Json.AsyncApi.CodeGeneration - 93.3%
Corvus.Text.Json.AsyncApi.Kafka - 0.3%
Corvus.Text.Json.AsyncApi.Mqtt - 0.5%
Corvus.Text.Json.AsyncApi.Nats - 0.3%
Corvus.Text.Json.AsyncApi.Polly - 100%
Corvus.Text.Json.AsyncApi.Testing - 96%
Corvus.Text.Json.AsyncApi.WebSocket - 0%
Corvus.Text.Json.CodeGeneration - 93.2%
Corvus.Text.Json.JMESPath - 93.8%
Corvus.Text.Json.JMESPath.CodeGeneration - 97.7%
Corvus.Text.Json.Jsonata - 91.6%
Corvus.Text.Json.Jsonata.CodeGeneration - 88.5%
Corvus.Text.Json.JsonLogic - 95.1%
Corvus.Text.Json.JsonLogic.CodeGeneration - 92.8%
Corvus.Text.Json.JsonPath - 94.7%
Corvus.Text.Json.JsonPath.CodeGeneration - 96.1%
|
Contributor
Code Coverage Summary Report - Windows (net481)Summary
CoverageCorvus.Json.CodeGeneration - 80.6%
Corvus.Json.CodeGeneration.CSharp - 81.5%
Corvus.Json.ExtendedTypes - 72.6%
Corvus.Json.JsonReference - 73.8%
Corvus.Text.Json - 92.7%
Corvus.Text.Json.CodeGeneration - 86.2%
Corvus.Text.Json.JMESPath - 93.8%
Corvus.Text.Json.JMESPath.CodeGeneration - 97.7%
Corvus.Text.Json.Jsonata - 91.8%
Corvus.Text.Json.Jsonata.CodeGeneration - 88.6%
Corvus.Text.Json.JsonLogic - 95.1%
Corvus.Text.Json.JsonLogic.CodeGeneration - 92.8%
Corvus.Text.Json.JsonPath - 95%
Corvus.Text.Json.JsonPath.CodeGeneration - 96.1%
Corvus.Text.Json.Patch - 97.5%
Corvus.Text.Json.Toon - 88.2%
Corvus.Text.Json.Validator - 92.4%
Corvus.Text.Json.Yaml - 90.8%
Corvus.Toon.SystemTextJson - 90%
Corvus.Yaml.SystemTextJson - 88.7%
|
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the OpenAPI and AsyncAPI generation work from the workflow-engine campaign back to the mainline, and merges two community contributions on top of it.
Cut as 5.3.0 rather than 5.2.14: three changes are breaking.
GitVersion.ymlcarries thenext-versionbump.Community contributions
Both from Levy Barbosa (@Levyks), with thanks. Merged as true merges, so his commits are in this history with his authorship intact rather than squashed away.
MessageContext.Both needed conflict resolution, because this branch had already refactored the code they were written against. In each case the contribution's logic was moved onto the newer shape rather than the shape being reverted:
StartAsyncbody; those had been collapsed into one, so it is now written once and also covers the responder form. That needed aMessageContextoverload ofSubscribeReplyAsync, which did not exist when Pass binding context to generated AsyncAPI consumers #913 was written.string.Replaceper parameter plus a concat for the dead-letter address. It now splits the template at generation time so the literal parts areu8literals, transcodes only the parameter values, and fills the address once — allocating only the two arrays the subscription keeps. AReadOnlySpan<char>overload sits under thestringone.#914 also fixes a live bug: a consumer for
orders.{orderId}.createdpreviously subscribed to that address literally, so it listened on a channel no publisher ever wrote to. The shipped streetlights recipes and 21 runtime tests were all asserting that behaviour.Both reach AsyncAPI 2.6 as well as 3.0, because
AsyncApi26CodeGeneratordelegates emission to the 3.0 generator.Breaking changes
IMessageTransport.RequestAsynctakes aJsonWorkspace— required parameter, so every call site and every custom transport implementation changes.Ok()is gone; handlers must supply bytes or a writer.StartAsync()no longer compiles for such a channel.Each is a compile break, deliberately: in all three cases the code that compiled before was expressing something the API could not actually do.
Verification
That last one is the run that matters for this release. Every transport — NATS, AMQP, Kafka, Azure Service Bus, MQTT, WebSocket — exercises both
RequestAsyncandSubscribeReplyAsync, so the breaking workspace parameter and the new responder capability are proven against live brokers rather than against mocks and generated text.Not included
The campaign's Arazzo work, and the repo-root files that only serve it:
Directory.Packages.props(45 Arazzo-only packages), both CI workflows,CLAUDE.md,.gitignore, coverage settings..github/skillsandcopilot-instructions.mdare included, since they document the conventions these generators emit against.🤖 Generated with Claude Code
https://claude.ai/code/session_016wwPnkvEn24dgt5T8mHGJq