Skip to content

v9: AsyncAPI 3.0 document generation - #2280

Draft
slang25 wants to merge 12 commits into
slang25/v9-cloudeventsfrom
slang25/v9-asyncapi
Draft

v9: AsyncAPI 3.0 document generation#2280
slang25 wants to merge 12 commits into
slang25/v9-cloudeventsfrom
slang25/v9-asyncapi

Conversation

@slang25

@slang25 slang25 commented Aug 12, 2026

Copy link
Copy Markdown
Member

v9 stacked PR — AsyncAPI document generation

Adds a JustSaying.AsyncApi package that generates an AsyncAPI 3.0 document describing an app's publications and subscriptions — the messaging equivalent of ASP.NET Core's OpenAPI document. Stacked on #2184 (which is stacked on #2183#2182) — review/merge those first.

As far as I can tell no .NET messaging library has a first-party AsyncAPI story (MassTransit and aws-dotnet-messaging have nothing, NServiceBus has demo-only samples), so this would be a nice differentiator.

The metadata seam (core)

  • The fluent builders now record each publication/subscription (message CLR type, destination kind + resolved name, direction, subscription group, wire name) into an IMessagingMetadataRegistry — resolved via the IServiceResolver their Configure methods already take, so it's fully opt-in and costs nothing when unused.
  • Everything is captured at configure time, before the bus starts and before any AWS call. This deliberately does not build on Interrogate() — that's diagnostic-only, keyed by Type.Name, and only populated after start.

The package

  • services.AddJustSayingAsyncApi(o => { o.Title = "Orders"; ... }) — registers the registry and an IAsyncApiDocumentProvider.
  • Mapping: publication → channel + send operation; subscription → channel + receive operation; a multi-type queue (v9 (3/3): CloudEvents and multi-type-per-queue subscriptions #2184) → one channel with a messages map, which is exactly the AsyncAPI 3.0 model; CloudEvents registrations name messages by their CE type. Dynamic (WithTopicName(Func<...>)) destinations have no static address, so they're skipped.
  • Payload schemas come from JsonSchemaExporter, driven by the app's actual serializer options — so the schema reflects the real wire contract, including the source-generated STJ path from v9 (2/3): Native AOT support #2183.
  • Document model is ByteBard.AsyncAPI.NET (the official continuation of LEGO.AsyncAPI.NET) — core package only, zero transitive deps on net8.0.

Where this is heading

The provider interface (JustSaying.AsyncApi.IAsyncApiDocumentProvider) has a deliberately stable full name and GetDocumentNames()/GenerateAsync(name, TextWriter) shape: it mirrors how ASP.NET Core's build-time OpenAPI tooling resolves IDocumentProvider by name from the app's captured host. I've spiked that mechanism (HostFactoryResolver + stopApplication: true — hosted services never start, so bus.StartAsync/AWS never runs) and it works; the follow-up PR is the .targets + insider tool packaging for dotnet build-time generation.

Outstanding actions:

10 new tests + 303/304 unit (one pre-existing flaky channels test) + 10 CloudEvents green; solution builds clean with analyzers on.

Stack

  1. v9 (1/3): Drop the Message base-class constraint #2182 — Foundation
  2. v9 (2/3): Native AOT support #2183 — Native AOT
  3. v9 (3/3): CloudEvents and multi-type-per-queue subscriptions #2184 — CloudEvents + multi-type-per-queue
  4. this PR — AsyncAPI (base: slang25/v9-cloudevents)

🤖 Generated with Claude Code

Adds a new JustSaying.AsyncApi package that generates an AsyncAPI 3.0
document describing the bus's publications and subscriptions, using
ByteBard.AsyncAPI.NET for the document model and JsonSchemaExporter for
payload schemas.

Core gains a metadata registry seam: the fluent builders record each
publication and subscription (message CLR type, destination kind and
resolved name, direction, subscription group, wire name) into an
IMessagingMetadataRegistry when one is registered, at configure time and
before any AWS infrastructure is provisioned. The registry is opt-in via
the existing IServiceResolver parameter, so nothing changes for apps
that don't use it.

Mapping: each publication becomes a channel plus a send operation, each
subscription a channel plus a receive operation; multi-type queues
become one channel with a messages map; CloudEvents registrations name
messages by their CloudEvents type. Dynamic (per-message) destinations
have no static address and are skipped.

The IAsyncApiDocumentProvider service has a stable full name so a
future GetDocument.Insider-style build-time tool can resolve it by name
from a captured host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces first-party AsyncAPI 3.0 generation using messaging metadata captured during bus configuration. This PR builds on the v9 CloudEvents and multi-type queue work from #2184.

Changes:

  • Adds publication/subscription metadata capture across fluent builders.
  • Adds the JustSaying.AsyncApi package, schema generation, and document provider.
  • Adds CloudEvents, multi-type queue, and metadata registry tests.

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/JustSaying.AsyncApi.Tests/WhenGeneratingWithCloudEventsAndMultiTypeQueues.cs Tests CloudEvents and multi-type queues.
tests/JustSaying.AsyncApi.Tests/WhenGeneratingAnAsyncApiDocument.cs Tests core document generation.
tests/JustSaying.AsyncApi.Tests/MessagingMetadataRegistryTests.cs Tests metadata deduplication and region capture.
tests/JustSaying.AsyncApi.Tests/JustSaying.AsyncApi.Tests.csproj Defines the AsyncAPI test project.
src/JustSaying/PublicAPI/PublicAPI.Unshipped.txt Records new core public APIs.
src/JustSaying/Messaging/Metadata/SubscriptionMetadata.cs Models subscription metadata.
src/JustSaying/Messaging/Metadata/PublicationMetadata.cs Models publication metadata.
src/JustSaying/Messaging/Metadata/MessagingMetadataRegistry.cs Stores and deduplicates metadata.
src/JustSaying/Messaging/Metadata/MessagingDestinationKind.cs Defines destination types.
src/JustSaying/Messaging/Metadata/MessageTypeMetadata.cs Models message type metadata.
src/JustSaying/Messaging/Metadata/IMessagingMetadataRegistry.cs Defines the metadata registry contract.
src/JustSaying/Messaging/MessageSerialization/SystemTextJsonSerializationFactory.cs Exposes serializer options.
src/JustSaying/Fluent/TopicSubscriptionBuilder\1.cs` Captures topic subscription metadata.
src/JustSaying/Fluent/TopicPublicationBuilder\1.cs` Captures topic publication metadata.
src/JustSaying/Fluent/TopicAddressPublicationBuilder\1.cs` Captures addressed-topic metadata.
src/JustSaying/Fluent/QueueSubscriptionBuilder\1.cs` Captures queue subscription metadata.
src/JustSaying/Fluent/QueuePublicationBuilder\1.cs` Captures queue publication metadata.
src/JustSaying/Fluent/QueueAddressSubscriptionBuilder\1.cs` Captures addressed-queue subscriptions.
src/JustSaying/Fluent/QueueAddressPublicationBuilder\1.cs` Captures addressed-queue publications.
src/JustSaying/Fluent/MultiTypeQueueSubscriptionBuilder.cs Captures multi-type queue metadata.
src/JustSaying.CloudEvents/PublicAPI/PublicAPI.Unshipped.txt Records exposed CloudEvents APIs.
src/JustSaying.CloudEvents/CloudEventsServiceCollectionExtensions.cs Registers CloudEvents options.
src/JustSaying.CloudEvents/CloudEventSerializationFactory.cs Exposes the payload serializer factory.
src/JustSaying.CloudEvents/CloudEventOptions.cs Exposes CloudEvents type lookup.
src/JustSaying.AsyncApi/PublicAPI/net8.0/PublicAPI.Unshipped.txt Defines the new package API surface.
src/JustSaying.AsyncApi/PublicAPI/net8.0/PublicAPI.Shipped.txt Establishes the shipped API baseline.
src/JustSaying.AsyncApi/JustSaying.AsyncApi.csproj Defines the shipping package.
src/JustSaying.AsyncApi/JsonSchemaNodeMapper.cs Maps exported JSON schemas.
src/JustSaying.AsyncApi/IAsyncApiDocumentProvider.cs Defines document generation tooling API.
src/JustSaying.AsyncApi/AsyncApiServiceCollectionExtensions.cs Registers AsyncAPI services.
src/JustSaying.AsyncApi/AsyncApiOptions.cs Defines document options.
src/JustSaying.AsyncApi/AsyncApiDocumentProvider.cs Builds and serializes documents.
src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs Generates AsyncAPI channels and operations.
JustSaying.slnx Adds the package and tests to the solution.
Directory.Packages.props Adds AsyncAPI and JSON dependencies.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs Outdated
Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs Outdated
Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs Outdated
Comment thread src/JustSaying.AsyncApi/JsonSchemaNodeMapper.cs
Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentProvider.cs Outdated
Comment thread src/JustSaying/Messaging/Metadata/MessagingMetadataRegistry.cs
slang25 and others added 2 commits August 12, 2026 14:19
…subscription

The address-based builders (topic ARN, queue ARN/URL) previously called
SetRegion with the destination's region, so whichever destination was
configured first silently became the document-wide region and every other
destination was documented against it. The registry's region is now only
set from the bus's configured region, and explicitly-addressed
destinations carry their own region on the metadata entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Channels are keyed by destination identity (address, kind, region), so a
  topic and queue sharing a name, or the same name in two regions, no longer
  collapse into one channel with the wrong server reference.
- Send/receive operations merge the message types of every registration that
  targets the same destination instead of the last one overwriting the rest.
- Servers are generated per region, and destinations addressed in another
  region reference a region-suffixed server rather than the bus default.
- Recursive schemas: the exporter's local $ref pointers are resolved and
  inlined (cycles emit an empty schema) instead of being dropped.
- No payload schema is inferred for a Newtonsoft or custom serialization
  factory, since System.Text.Json-shaped schemas can misdocument the wire
  contract; an explicit AsyncApiOptions.SerializerOptions still applies.
- The document provider observes its cancellation token when writing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:175

  • The generated AWS hostname is incorrect outside the standard aws partition. For example, cn-north-1 requires the .amazonaws.com.cn suffix (the existing SqsEndpointHelper.cs:5-12 already handles this), but this emits .amazonaws.com; the SQS block below has the same defect. Derive both hosts from the region's partition/DNS suffix instead of hard-coding it.
            document.Servers[ServerKey("sns", region, primaryRegion)] = new AsyncApiServer()
            {
                Host = $"sns.{region}.amazonaws.com",
                Protocol = "sns",
                Description = $"Amazon SNS in {region}.",

src/JustSaying/Fluent/QueueAddressSubscriptionBuilder`1.cs:152

  • This metadata drops the per-subscription serializer selected by WithMessageBodySerializer (used at line 120). The generator therefore derives a payload schema from the global serialization factory even when this queue is actually deserialized by a custom serializer with a different wire contract. Capture schema/serializer information for this registration, or suppress inferred payload schemas when an override is present.
            metadataRegistry.AddSubscription(new SubscriptionMetadata(
                queue.QueueName,
                topicName: null,
                attachedQueueConfig.SubscriptionGroupName,
                attachedQueueConfig.RawMessageDelivery,
                [new MessageTypeMetadata(typeof(T), bus.MessageTypeRegistry.GetLogicalName(typeof(T)))],
                _queueAddress.RegionName));

src/JustSaying/Fluent/QueueAddressPublicationBuilder`1.cs:157

  • QueueAddressQueue accepts queue URLs with a trailing slash by ignoring empty path segments, but this last-segment lookup returns an empty destination name for the same valid URL. That produces an empty channel key/address in the AsyncAPI document. Select the last non-empty trimmed segment instead.
            metadataRegistry.AddPublication(new PublicationMetadata(
                MessagingDestinationKind.SqsQueue,
                _queueAddress.QueueUrl.Segments[_queueAddress.QueueUrl.Segments.Length - 1].TrimEnd('/'),
                isDynamic: false,
                [new MessageTypeMetadata(typeof(T), subject)],
                _queueAddress.RegionName));

Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs
Comment thread src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs Outdated
slang25 and others added 3 commits August 12, 2026 14:55
Two AsyncAPI object keys could still collide silently.

Message keys were derived from the sanitized wire name each time they
were needed, so two distinct wire names that sanitize to the same key
(orders/placed and orders_placed) overwrote each other in the channel's
messages map, and the operation merge treated them as one message. Each
channel now allocates a key per wire name and operation references read
that allocation, so a reference always resolves to the message the
channel holds. The merge dedupes on the wire name rather than the key.

The channel key fallback chain stopped after appending the region, and
that last candidate was used without checking whether it belonged to
another destination, so a queue genuinely named orders-queue-us-east-1
could have its channel taken over by a cross-region orders queue. Both
fallbacks now run through a shared allocator that keeps appending a
deterministic numeric suffix until the key is free or already owned by
the same destination identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Defer AWS credential resolution to request time in DefaultAwsClientFactory,
  so building the bus (and generating a document) works without resolvable
  credentials, such as on CI
- Root ByteBard's reflectively-created enum arrays so serialization works
  under Native AOT
- Take an optional ILogger and warn when parts of the document are omitted
  (dynamic publications, underivable payload schemas) or the document is empty
- Document the CloudEvents 1.0 envelope as the payload with the data schema
  nested under 'data', and describe raw vs SNS-enveloped delivery per
  subscription
- Expand closed generic type names in titles and summaries
- Rethrow bus-build failures from the document provider with an error that
  explains handlers must be registered even for documentation-only entry points
- Correct the spec version wording to AsyncAPI 3.1 and pin it with a test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ild time

Referencing the package makes dotnet build write asyncapi.json next to the
project, mirroring ASP.NET Core's build-time OpenAPI generation. The MSBuild
target runs a GetDocument tool via dotnet exec with the application's
deps.json and runtimeconfig.json; the tool captures the application's host
with HostFactoryResolver (aborted at Build(), so the host never starts and no
AWS calls are made) and resolves IAsyncApiDocumentProvider by name via
reflection, so the tool and runtime package versions are decoupled.

- Configurable via JustSayingAsyncApiGenerateDocumentsOnBuild (default true)
  and JustSayingAsyncApiDocumentsDirectory (default: the project directory)
- Incremental: skipped when the app hasn't rebuilt, and the document is only
  rewritten when its content changes, keeping timestamps stable
- Generation failures surface as first-class build errors in canonical
  MSBuild format, without the MSB3073 command-line dump
- Integration tests spawn the tool against a fixture app exactly as the
  targets do, covering the happy path, no-rewrite behaviour, and the
  missing-handler and missing-registration failure modes
- build.ps1 now packs JustSaying.AsyncApi and JustSaying.AsyncApi.BuildTools
  and runs JustSaying.AsyncApi.Tests, which were missing from CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@slang25
slang25 force-pushed the slang25/v9-asyncapi branch from 067985f to 12ec1f3 Compare August 12, 2026 23:30
Exercising JustSaying.AsyncApi.BuildTools across a wider scenario matrix
(web apps, legacy hosts, CLI-style entry points, multi-targeting, test
projects, shared documentation directories) surfaced four rough edges,
fixed here after comparing with how Microsoft.Extensions.ApiDescription.Server
handles the same problems:

- Skip test projects: modern test frameworks make test projects
  executables, and one referencing this package (e.g. via
  Directory.Build.props) would run its whole test suite inside the build
  before failing. The after-build hooks now exclude IsTestProject.
- Cap the entry point wait: an application that blocks before building
  its host stalled the build for HostFactoryResolver's five-minute
  default with no output. A new JustSayingAsyncApiEntryPointTimeoutSeconds
  property (default 60) flows to the tool, and the timeout error names
  the property and the escape hatch (the entry assembly is
  'JustSaying.AsyncApi.GetDocument' during generation).
- Make the file name configurable: two projects pointing
  JustSayingAsyncApiDocumentsDirectory at the same folder silently
  overwrote each other's asyncapi.json. A new
  JustSayingAsyncApiDocumentFileName property (default 'asyncapi') maps
  to a --file-name tool argument with ApiDescription.Server's naming
  scheme; shared directories can use $(MSBuildProjectName).
- Generate once for multi-targeted projects: each inner build previously
  ran the tool against the same output file. New buildMultiTargeting
  targets mirror ApiDescription.Server: the outer build generates for the
  first .NET 8+ framework and the inner-build hook is disabled.

The generation target is also split into an invocable
GenerateJustSayingAsyncApiDocuments target plus a private after-build
hook, so it can be run manually when on-build generation is disabled,
and the host-build failure error now mentions that the entry point is
invoked with ['--applicationName', '<assembly name>'] rather than empty
arguments, which otherwise makes CLI-style entry points fail confusingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 56 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

src/JustSaying/Messaging/Metadata/PublicationMetadata.cs:25

  • SQS publications are not always the bare serializer payload represented by this metadata. Both queue publication builders wrap non-raw messages as { "Message": "<serialized JSON>", "Subject": "..." } (OutboundMessageConverter.cs:51-57), while WithRawMessages() and self-describing serializers do not. Because PublicationMetadata records no wire mode, the generator always emits the CLR payload schema and therefore misdocuments every default SQS publication. Record the effective raw/envelope mode here, populate it from both queue builders, and generate the corresponding envelope schema.
    src/JustSaying.AsyncApi.BuildTools/build/JustSaying.AsyncApi.BuildTools.targets:49
  • The tool writes generated paths into this cache, but only the cache itself is registered in FileWrites. Consequently dotnet clean leaves all generated AsyncAPI documents behind. Read the cache into an item list and register those paths too.
    <ItemGroup>
      <FileWrites Include="$(_JustSayingAsyncApiFileListPath)" />
    </ItemGroup>

build.ps1:29

  • JustSaying.AsyncApi has a package-producing project reference to JustSaying.CloudEvents, but the release pack loop still never packs the shipping CloudEvents project. The build will produce an AsyncApi package whose declared dependency is absent from the release artifacts. Add CloudEvents to libraryProjects before packing AsyncApi.
    (Join-Path $solutionPath "src" "JustSaying.AsyncApi" "JustSaying.AsyncApi.csproj"),

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:203

  • This hard-coded DNS suffix produces an invalid SNS host for China regions (for example cn-north-1 requires amazonaws.com.cn). Resolve the service endpoint from AWS region metadata instead of assuming the commercial partition suffix.
                Host = $"sns.{region}.amazonaws.com",

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:213

  • This assumes every queue is hosted at the standard AWS endpoint. QueueAddress.FromUrl explicitly supports custom URLs such as LocalStack; those registrations currently become sqs.unknown.amazonaws.com, and China regions also require a different DNS suffix. Preserve the explicit endpoint in metadata and otherwise resolve the regional SQS endpoint from AWS metadata.
                Host = $"sqs.{region}.amazonaws.com",

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:14

  • The PR title and description promise AsyncAPI 3.0 generation, but this generator is documented and tested as emitting 3.1.0. Either emit the advertised 3.0 document or update the PR/package-facing description to 3.1 so consumers select the correct validator and tooling.
/// Generates an AsyncAPI 3.1 document from the publications and subscriptions captured in an
/// <see cref="IMessagingMetadataRegistry"/>.

slang25 and others added 2 commits August 13, 2026 09:43
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An outer build invoked with a runtime identifier only produces RID-specific
outputs, so the inner generation invocation must evaluate the same global
properties to find the assembly, deps file, and runtimeconfig the build just
wrote. RemoveProperties="RuntimeIdentifier" (copied from
Microsoft.Extensions.ApiDescription.Server) pointed generation at non-RID
paths that were never built, failing any clean 'dotnet build -r <rid>' of a
multi-targeted project.

Reproducing that also surfaced a second bug in the same target: the
batched-PropertyGroup guard intended to pick the first supported target
framework actually kept the last one (net10.0 for 'net8.0;net10.0'). The
supported frameworks are now collected into an item and the first is taken
deterministically.

Verified against the packed nupkg with the MultiTargetWorker scenario:
clean 'dotnet build -r osx-arm64 --self-contained false' now generates from
the RID-specific net8.0 outputs, and plain/incremental builds are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 55 out of 56 changed files in this pull request and generated 4 comments.

Suppressed comments (6)

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:213

  • This SQS hostname is also wrong outside the standard AWS partition; China queue URLs use the amazonaws.com.cn suffix, and explicit queue URLs may use another host entirely. Capture/resolve the actual endpoint rather than rebuilding it as sqs.{region}.amazonaws.com.
                Host = $"sqs.{region}.amazonaws.com",

build.ps1:34

  • The CI script only runs projects in $testProjects; JustSaying.CloudEvents.Tests is not listed, so the CloudEvents suite in this stacked change never executes in CI. Include it alongside the new AsyncApi suite.
    (Join-Path $solutionPath "tests" "JustSaying.AsyncApi.Tests" "JustSaying.AsyncApi.Tests.csproj"),

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:73

  • During build-time generation the process entry assembly is JustSaying.AsyncApi.GetDocument, not the loaded application (the tool itself notes this in Program.cs:116-117). If the user leaves Title unset, the generated document is therefore titled after the tool instead of the application. Resolve the captured host's application name or pass the application assembly name into generation.
                Title = _options.Title ?? Assembly.GetEntryAssembly()?.GetName().Name ?? "JustSaying application",

src/JustSaying.AsyncApi.BuildTools/build/JustSaying.AsyncApi.BuildTools.targets:19

  • The comment documents direct invocation via dotnet msbuild -t:GenerateJustSayingAsyncApiDocuments, but this target has no build dependency. On a clean checkout, TargetPath, the deps file, and the runtimeconfig have not been produced, so the documented command fails. Either make a direct-invocation wrapper build the selected target framework first or document that callers must build first.
  <Target Name="GenerateJustSayingAsyncApiDocuments"
          Inputs="$(TargetPath);$(ProjectDepsFilePath);$(ProjectRuntimeConfigFilePath)"
          Outputs="$(_JustSayingAsyncApiFileListPath)">

src/JustSaying.AsyncApi/AsyncApiDocumentGenerator.cs:203

  • Hard-coding amazonaws.com produces an invalid SNS endpoint for supported non-default AWS partitions; for example arn:aws-cn:sns:cn-north-1:... is actually served under amazonaws.com.cn. Preserve the ARN partition/endpoint metadata or resolve the hostname through the AWS SDK instead of constructing it from only the region.

This issue also appears on line 213 of the same file.

                Host = $"sns.{region}.amazonaws.com",

src/JustSaying.CloudEvents/CloudEventsServiceCollectionExtensions.cs:38

  • Because this uses TryAdd, a previously registered CloudEventOptions instance may differ from the local options captured by the serialization-factory lambda below. AsyncApi resolves the registered instance while the serializer uses the local one, so the documented CloudEvents type names can differ from the emitted wire values. Resolve CloudEventOptions from the service provider when constructing the factory.
        services.TryAddSingleton(options);

Comment thread build.ps1
Comment thread src/JustSaying/Fluent/QueuePublicationBuilder`1.cs Outdated
Comment thread src/JustSaying/Fluent/QueueAddressPublicationBuilder`1.cs Outdated
Comment thread src/JustSaying.AsyncApi.BuildTools/Program.cs
slang25 and others added 3 commits August 17, 2026 10:37
JustSaying.AsyncApi takes a package dependency on JustSaying.CloudEvents, but
CI only packs the projects listed in $libraryProjects and publishes those
nupkg files, so a release would have shipped an AsyncApi package whose
CloudEvents dependency did not exist on NuGet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Publishing to an SQS queue without raw messages wraps the payload in
JustSaying's { "Message", "Subject" } envelope, so the SQS body is not the
payload the document describes. That is the default, and the publication
metadata did not carry the effective raw-message setting at all, so the
generated document silently claimed the bare payload was the wire body.

PublicationMetadata now captures whether the envelope is used, resolved from
the same combination the publisher itself uses (the builder's WithRawMessages,
the write configuration's IsRawMessage, and a self-describing serializer),
and the send operation carries a description of the envelope, mirroring how
subscriptions describe SNS raw versus enveloped delivery.

The envelope is described rather than modelled as the payload schema because
the payload survives inside it as a JSON-encoded string, and the AsyncAPI
schema model has no contentSchema keyword to nest the real schema under; the
message schema stays the useful one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GenerateAsync builds the bus, exports the schemas, and runs PostProcess before
its first await, so invoking it on the calling thread left all of that outside
the two-minute timeout: anything slow or blocked there hung the build
indefinitely instead of failing with the timeout error. The invocation now
happens on the task being waited on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants