AsyncAPI Document Generation
Brighter can automatically generate AsyncAPI 3.0 documents from your service's runtime configuration. This gives you machine-readable documentation of your messaging contracts — the topics your service publishes to and subscribes from, along with JSON Schema for each message type — without maintaining separate documentation that drifts out of date.
What is AsyncAPI?
AsyncAPI is an open specification for defining asynchronous APIs. It serves the same role for message-driven architectures that OpenAPI (Swagger) serves for REST APIs: it describes what messages a service sends and receives, what topics or queues are involved, and what the message payloads look like.
Without AsyncAPI, teams maintain messaging documentation manually — in wikis, READMEs, or Confluence pages. This documentation inevitably becomes stale as the code evolves. AsyncAPI solves this by providing a standard, machine-readable format that can be generated directly from your code.
Brighter generates AsyncAPI 3.0 documents by inspecting your registered subscriptions, publications, and message types. The generated document can be used with the broader AsyncAPI ecosystem — tools like AsyncAPI Studio for visualization, Microcks for mock generation, and code generators for consumer SDKs.
Getting Started
Prerequisites
A Brighter service with an external bus configured
.NET 8.0 or later
Two NuGet packages:
dotnet add package Paramore.Brighter.AsyncAPI
dotnet add package Paramore.Brighter.AsyncAPI.NJsonSchemaThe core package (Paramore.Brighter.AsyncAPI) contains the document generator. The schema package (Paramore.Brighter.AsyncAPI.NJsonSchema) provides the default JSON Schema generator using NJsonSchema. If you implement your own IAmASchemaGenerator, you do not need the NJsonSchema package — but you must register your implementation before calling UseAsyncApi(), otherwise UseAsyncApi() throws an InvalidOperationException.
Adding AsyncAPI Generation
Register AsyncAPI generation by calling UseAsyncApi() on your Brighter builder:
Then generate the document after building your host:
This writes both asyncapi.json and asyncapi.yaml to the output path. The JSON file is the canonical output; the YAML file is derived from it for human readability.
How Brighter Discovers Your Messaging Contracts
Brighter collects messaging contracts from three sources, processed in this priority order:
Subscriptions — topics your service consumes (registered via
AddConsumers())Publications — topics your service produces (registered via
AddProducers())Assembly scanning — IRequest types decorated with
[PublicationTopic]
Deduplication is keyed on (channel, action). Subscriptions produce receive operations; publications and assembly-scanned [PublicationTopic] types produce send operations. Sources are processed in the order above, so DI-registered publications suppress assembly-scanned sends on the same routing key. A subscription and a publication on the same routing key are not duplicates — they produce one channel with both a receive and a send operation.
Subscriptions (Receive Operations)
Subscriptions registered with AddConsumers() become receive operations in the AsyncAPI document. Each subscription's RoutingKey becomes a channel address, and the RequestType provides the message schema.
This produces a channel at address payment.received with a receive operation and a PaymentReceivedEvent message schema.
Send-only applications that do not call AddConsumers() produce documents with no receive operations — this is not an error.
Publications (Send Operations)
Publications registered in the producer registry become send operations. Each publication's Topic becomes the channel address. Use typed publications (e.g. RmqPublication<T>) to associate a RequestType with the publication for schema generation.
Receive-only applications that do not register producers produce documents with no send operations.
Assembly Scanning (Send Operations)
You can decorate your Event or Command types with the [PublicationTopic] attribute to make them discoverable via assembly scanning:
Assembly scanning discovers concrete, non-abstract types that implement IRequest and carry this attribute. The attribute's topic string becomes the channel address in the generated document.
Assembly scanning is useful when you want your message types to self-document their publication topics without requiring them to be wired into a producer registry. You can disable it with:
Deduplication
When the same routing key appears in multiple sources, Brighter produces one channel with multiple operations rather than duplicate channels:
Same routing key from a subscription and a publication → one channel, one
receiveoperation, onesendoperation (different actions, so not a duplicate)Same
IRequesttype in both a subscription and a publication → one message component incomponents/messagesSame routing key as both a DI publication and an assembly-scanned
[PublicationTopic]type → DI publication wins, assembly scan is skipped (both aresend)A subscription on a routing key does not suppress an assembly-scanned
sendon the same key — they have different actions, so both are emitted
Configuration
AsyncApiOptions
Configure the generated document via the UseAsyncApi() delegate:
Title
string
"Brighter Application"
The info.title field in the generated document
Version
string
"1.0.0"
The info.version field
Description
string?
null
Optional info.description field
Servers
Dictionary<string, V3ServerDefinition>?
null
Server definitions (broker endpoints)
AssembliesToScan
IEnumerable<Assembly>?
Entry assembly
Assemblies to scan for [PublicationTopic] types
DisableAssemblyScanning
bool
false
When true, skips assembly scanning entirely
SupplementalPublications
IEnumerable<Publication>?
null
Additional publications to include beyond those in the producer registry
Full configuration example:
Output Formats
GenerateAsyncApiDocumentAsync() writes two files:
JSON — written to the path you specify (e.g.
asyncapi.json). This is the canonical format.YAML — written alongside with a
.yamlextension (e.g.asyncapi.yaml). Derived from the JSON output for guaranteed consistency.
Both files describe the same document. Use JSON for machine consumption and YAML for human review.
Custom Schema Generation
The default schema generator uses NJsonSchema to produce JSON Schema from your IRequest types. It honours System.ComponentModel.DataAnnotations attributes (e.g. [Required], [StringLength]) and System.Text.Json attributes (e.g. [JsonPropertyName]).
To substitute your own schema generation logic, implement IAmASchemaGenerator:
Register your implementation before calling UseAsyncApi():
When UseAsyncApi() finds an existing IAmASchemaGenerator registration, it does not override it with the default NJsonSchema implementation.
CI/CD Integration
A common pattern is to generate the AsyncAPI document as part of your CI pipeline using a command-line argument:
The host must be built (so DI is resolved) but does not need to start running. No broker connection is required for RabbitMQ-based services. For Kafka, see the Kafka example below for a pattern that avoids broker connections during generation.
If UseAsyncApi() was never called, GenerateAsyncApiDocumentAsync() throws an InvalidOperationException because neither IAmAnAsyncApiDocumentGenerator nor IAsyncApiDocumentWriter will be registered. The output path is also normalised: if you pass a path without a .json extension it is appended, and the YAML file is always written alongside with the .yaml extension.
You can validate the generated document using the AsyncAPI CLI:
Complete Examples
RabbitMQ Example
This example configures a service with one subscription (receiving payment events) and one publication (sending order events), then generates the AsyncAPI document:
Running dotnet run -- --generate-asyncapi produces the following document (abbreviated):
Kafka Example
Kafka has a specific consideration: KafkaProducerRegistryFactory.Create() opens a real broker connection at construction time. When generating the AsyncAPI document, you typically don't have a live broker available (especially in CI). The solution is to conditionally skip producer registry creation and rely on [PublicationTopic] assembly scanning to discover publications:
The key difference is the if (!generateAsyncApi) guard around producer registry creation. Because OrderCreatedEvent carries [PublicationTopic("order.created")], assembly scanning discovers it and the generated document still correctly describes the send operation — without needing a live Kafka broker.
Further Reading
AsyncAPI Specification — the full AsyncAPI 3.0 specification
AsyncAPI Studio — browser-based tool for visualizing AsyncAPI documents
AsyncAPI CLI — command-line tool for validation and code generation
Working samples:
Brighter/samples/AsyncAPI/RMQAsyncAPI/andBrighter/samples/AsyncAPI/KafkaAsyncAPI/Using an External Bus — prerequisite for AsyncAPI generation
Routing — understanding RoutingKey and topic configuration
Message Mappers — how messages are serialized for transport
RabbitMQ Configuration — RabbitMQ transport setup
Kafka Configuration — Kafka transport setup
Last updated
Was this helpful?
