Code-first AsyncAPI documentation
$ dotnet add package AsyncApi.Net.GeneratorIs an AsyncAPI documentation generator for dotnet.
ℹ Note that pre version 1.0.0, the API is regarded as unstable and breaking changes may be introduced.
This is a fork of the Sauner library, which was rewritten for the sake of ease of use and minimizing the cost of implementation in the project.
Install package from nuget
Configure base generator params in Program.cs:
services.AddAsyncApiSchemaGeneration(o =>
{
o.AssemblyMarkerTypes = new[] { typeof(StreetlightsController) }; // add assemply marker
o.AsyncApi = new AsyncApiDocument { Info = new Info { Title = "My application" }}; // introduce your application
});
Map generator and ui in Program.cs:
app.MapAsyncApiDocuments();
app.MapAsyncApiUi();
Set attributes to pub/sub methods or classes:
[PublishOperation<MyPayloadMessageType>("my_queue_name")]
[PublishOperation<MyPayloadMessageType, MySecondPayloadMessageType>("my_queue_second_name")]
public void MyMethod()
Run application, open endpoint /asyncapi/ui/ and view:

The overall concept looks like the following:

You can configure the basic document parameters and generator operation in the Program.cs file.
The AddAsyncApiSchemaGeneration method is used to add the generator, JSON serializer, and generator provider to the DI (Dependency Injection) container.
The method also takes an optional parameter setupAction, which is used to customize the library settings.
With it, you can configure:
IDocumentFilter interface to apply to the generated document.IOperationFilter interface to apply to the operations of the generated document.JsonSchemaGeneratorSettings class in the NJsonSchema library.Example:
services.AddAsyncApiSchemaGeneration(o =>
{
o.AssemblyMarkerTypes = new[] { typeof(StreetlightMessageBus) };
o.Middleware.UiTitle = "Streetlights API";
o.AsyncApi = new AsyncApiDocument
{
Info = new Info
{
Title = "Streetlights API",
Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.",
License = new License
{
Name = "Apache 2.0",
Url = "https://www.apache.org/licenses/LICENSE-2.0"
}
},
Servers = new()
{
["mosquitto"] = new Server
{
Url = "test.mosquitto.org",
Protocol = "mqtt",
},
["webapi"] = new Server
{
Url = "localhost:5000",
Protocol = "http",
},
},
};
});Additionally, to make it work, you need to add middleware and endpoints:
app.MapAsyncApiDocuments();
app.MapAsyncApiUi();The generator uses the PublishOperation and SubscribeOperation attributes as data sources, which can be added to any class/interface/method in any desired quantity with two constraints:
If the application has multiple subscribers to one channel, use the oneOf messages, for example:
[PublishOperation<LightMeasuredEvent, LightMeasuredEvent2, LightMeasuredEvent3>("PublishLightMeasuredTopic")]Or:
[PublishOperation("PublishLightMeasuredTopic", new TypeInfo[] { typeof(LightMeasuredEvent), typeof(LightMeasuredEvent2), typeof(LightMeasuredEvent3), typeof(LightMeasuredEvent4) } )]Unfortunately, this is a limitation of the AsyncAPI specification, which should be addressed in version 3.
Additionally, when specifying the attribute, you can provide various parameters for both the operation and the channel to which this operation belongs.
Channel parameters:
ChannelName - The name of the channel. The format depends on the conventions of the underlying messaging protocol. For example, AMQP uses dot-separated paths like 'light.measured'.ChannelDescription - An optional description of this channel item. CommonMark syntax can be used for rich text representation.ChannelBindingsRef - The name of a channel bindings item to reference. The bindings must be added to components/channelBindings with the same name.ChannelServers - The servers on which this channel is available, specified as an optional unordered list of names (string keys) of Server Objects defined in the Servers Object.Operation parameters:
MessagePayloadTypes - Message schema mark ID for matching with the message attribute. Can be specified as a generic.Summary - A short summary of what the operation is about.OperationId - Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.Description - A verbose explanation of the operation. CommonMark syntax can be used for rich text representation.BindingsRef - The name of an operation bindings item to reference. The bindings must be added to components/operationBindings with the same name.Tags - A list of tags for API documentation control. Tags can be used for logical grouping of operations.DocumentName - Name of the AsyncAPI document. More details are available in the section on multiple documents in one application.Examples:
[PublishOperation<AnotherSampleMesssage>("asw.sample_service.anothersample", OperationId = "AnotherSampleMessagePublisher", Summary = "Publish another sample.", ChannelDescription = "Another sample events.")]
[PublishOperation<SampleMessage>("messaging.sample", Summary = "Publish a sample message.", OperationId = "PublishSampleMessage", Description = "Publishes a sample message for demonstration purposes.", BindingsRef = "amqpBinding", Tags = new[] { "Messaging", "Publishing" })]In case the channel name contains the {*} construction, for example, qwerty.{my_id}.event, the generator will create a channel parameter. It will then attempt to find its description and schema in the components. By default, the schema string and an empty description are used.
Example:
AsyncApiOptions options = new()
{
AsyncApi = new()
{
Info = new()
{
Version = "1.0.0",
Title = GetType().FullName,
},
Components = new()
{
Parameters = new()
{
{
"tenant_id",
new()
{
Description = "The tenant identifier.",
Schema = NJsonSchema.JsonSchema.FromType(typeof(string)),
Location = "tester",
}
}
},
},
},
};
// ...
[SubscribeOperation<TenantCreated>("asw.tenant_service.{tenant_id}.{tenant_status}", OperationId = "OneTenantMessageConsumer", Summary = "Subscribe to domains events about a tenant.", ChannelDescription = "A tenant events.")]To add parameters to a message, the MessageAttribute is used.
It is applied to the DTO class or interface, where message parameters are specified:
HeadersType - The type used to generate the message headers schema.Name - A machine-friendly name for the message. Defaults to the generated schemaId.Title - A human-friendly title for the message.Summary - A brief summary of what the message is about.Description - A detailed explanation of the message. CommonMark syntax can be used for rich text representation.BindingsRef - The name of a message bindings item to reference. The bindings must be added to components/messageBindings with the same name.MessageId - A unique string used to identify the message. The id MUST be unique among all messages described in the API. The messageId value is case-sensitive. Tools and libraries MAY use the messageId to uniquely identify a message, therefore, it is RECOMMENDED to follow common programming naming conventions.Tags - A list of tags for API documentation control. Tags can be used for logical grouping of messages.Example:
[Message(HeadersType = typeof(MyMessageHeader), Title = "hello world")]
public record MyEvent(string content);You can create multiple AsyncApi documents within a single application in addition to the main one.
To achieve this:
Declare an additional document in Program.cs after adding the main configuration:
services.ConfigureNamedAsyncApi("Foo", asyncApi =>
{
asyncApi.Info = new Info()
{
Version = "1.0.0",
Title = "Foo",
};
asyncApi.Servers = new()
{
["mosquitto"] = new Server
{
Url = "test.mosquitto.org",
Protocol = "mqtt",
},
["webapi"] = new Server
{
Url = "localhost:5000",
Protocol = "http",
},
};
});
Declare operations for the document by specifying the DocumentName property in the attribute:
[PublishOperation<LightMeasuredEvent>(PublishLightMeasuredTopic, "Light", ChannelServers = new[] { "webapi" }, DocumentName = "Foo")]
In the running application, you can open a new document or its UI using the following paths:
!> this section is taken from Saunter, this functionality has not changed, it will be redesigned in future versions
Bindings are used to describe protocol specific information. These can be added to the AsyncAPI document and then applied to different components by setting the BindingsRef property in the relevant attributes [OperationAttribute], [MessageAttribute], [ChannelAttribute]
// Startup.cs
services.AddAsyncApiSchemaGeneration(options =>
{
options.AsyncApi = new AsyncApiDocument
{
Components =
{
ChannelBindings =
{
["my-amqp-binding"] = new ChannelBindings
{
Amqp = new AmqpChannelBinding
{
Is = AmqpChannelBindingIs.RoutingKey,
Exchange = new AmqpChannelBindingExchange
{
Name = "example-exchange",
VirtualHost = "/development"
}
}
}
}
}
}
});[Channel("light.measured", BindingsRef = "my-amqp-binding")] // Set the BindingsRef property
public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {}Available bindings:
!> this section is taken from Saunter, this functionality has not changed, it will be redesigned in future versions
The JSON schema generation can be customized using the options.JsonSchemaGeneratorSettings. Saunter defaults to the popular camelCase naming strategy for both properties and types.
For example, setting to use PascalCase:
services.AddAsyncApiSchemaGeneration(options =>
{
options.JsonSchemaGeneratorSettings.TypeNameGenerator = new DefaultTypeNameGenerator();
// Note: need to assign a new JsonSerializerSettings, not just set the properties within it.
options.JsonSchemaGeneratorSettings.SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver(),
Formatting = Formatting.Indented;
};
}You have access to the full range of both NJsonSchema and JSON.NET settings to configure the JSON schema generation, including custom ContractResolvers.
The current implementation has 3 goals.
asyncapi(with the possibility of updating to 3.0.0 after release)The main purpose of the stage works is to make it possible to describe an operation in 1 attribute without restrictions on the number of operations per method/class
To dotnet 7
To asyncapi 2.6.0
Set required and nullable props to schema
Give the opportunity to work with multiple operations in the one class/method
Kill channel attribute:
[SubscribeOperation("asw.tenant_service.tenants_history", OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.", ChannelDescription = "Tenant events.")]
public void PublishHelloWord(string content) { }
Rework message attribute:
[SubscribeOperation<BrokerHelloWorldDto>("asw.tenant_service.tenants_history", OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.", ChannelDescription = "Tenant events.")]
public void PublishHelloWord(string content) { }
[Message(Title = "Hello world, i`m class")]
public record BrokerHelloWorldDto(string content);
Kill channel params attribute (auto detect parameters from channel name)
[SubscribeOperation<BrokerHelloWorldDto>("asw.tenant_service.{tenants_name}", OperationId = "TenantMessageConsumer")]
public record BrokerHelloWorldDto(string content);
Redo the processing of multiple documents in the application (save default document with null name!!)
[SubscribeOperation<BrokerHelloWorldDto>("asw.tenant_service.{tenants_name}", OperationId = "TenantMessageConsumer", DocumentName = "Foo")]
[SubscribeOperation<BrokerHelloWorldDto>("asw.tenant_service.{tenants_name}", OperationId = "TenantMessageConsumer")]
public record BrokerHelloWorldDto(string content);
Rewrite usage docs:
Nuget package
Usability test on my environment Based on the results of the check in my environment. Using the library has become much more convenient, but there is not enough flexibility in implementation. Next, I will develop the library towards tools WITHOUT attributes. Example case:
public void SubsribeApplication() {
_js.SubByChannel<MyEvent1>("qwerty", _ => Console.Writeline("hello world"));
_js.SubByChannel<MyEvent2>("qwerty.123", _ => Console.Writeline("hello world"));
_js.SubByChannel<MyEvent3>("qwerty.zxc", _ => Console.Writeline("hello world"));
_js.SubByChannel<MyEvent4>("qwerty.qwerty", _ => Console.Writeline("hello world"));
}
// ...
private void SubsribeByChannel<TEvent>(this IJetStream js, string channel, Action<TEvent> handler) {
// honestly, I want to define the operation here.
// example:
// _operations += new PubOperation<TEvent>(channel);
js.PushSubscribe(channel, (_, m) => handler(_parser.From(m.Msg.Data)));
}
Release !!
Known limitations of the version that will be received at this stage:
The main goal of the stage works is to expand the automatically generated part of the schema through xml-comments and improve the quality of the product
yaml output documentTestHostThe main goal of this stage is to refine the remaining features of the async api (such as binding protocol) and develop a tool for describing detailed and complex schemes (without using attributes)
The main goal of this stage is to automatically generate part of the scheme from native library objects for the protocols I use (nats, signalR)
natssignalRswagger (or wait asyncapi 3.0.0 ...?)