For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

dotnet add package Paramore.Brighter.AsyncAPI
dotnet add package Paramore.Brighter.AsyncAPI.NJsonSchema

The 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:

  1. Subscriptions — topics your service consumes (registered via AddConsumers())

  2. Publications — topics your service produces (registered via AddProducers())

  3. Assembly scanningIRequest 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 receive operation, one send operation (different actions, so not a duplicate)

  • Same IRequest type in both a subscription and a publication → one message component in components/messages

  • Same routing key as both a DI publication and an assembly-scanned [PublicationTopic] type → DI publication wins, assembly scan is skipped (both are send)

  • A subscription on a routing key does not suppress an assembly-scanned send on the same key — they have different actions, so both are emitted

Configuration

AsyncApiOptions

Configure the generated document via the UseAsyncApi() delegate:

Property
Type
Default
Description

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 .yaml extension (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

Last updated

Was this helpful?