apigenie-generatorAsyncAPI serversserver aspnet (AsyncAPI)
server aspnet (AsyncAPI)
Generate an ASP.NET Core application of the Kafka channels of an AsyncAPI document.
Usage
apigenie-generator server aspnet [...OPTIONS] INPUT_FILE
Description
apigenie-generator server aspnet generates from OpenAPI documents (see
apigenie-generator server aspnet) and from AsyncAPI 3.x
documents, telling them apart by their openapi or asyncapi field. Documents referencing other
files are bundled first.
From an AsyncAPI document it generates an ASP.NET Core project (.NET 8, Microsoft.NET.Sdk.Web)
using the Kafka client Confluent.Kafka
(Apache-2.0, pinned at 2.15.1 in the project file). The consumers are hosted services of the
application, so the same project can also serve a REST API. By default the application is the one
the document describes: it sends the messages of send operations and receives the messages of
receive operations. Set the perspective option to counterpart to swap them.
The generator writes the Kafka channels: the channels of servers with the kafka or
kafka-secure protocol, and channels with a Kafka binding in documents without servers. Channels
of other protocols are skipped and reported as info.
public sealed class LabelFarm : IRequestLabelRenderHandler
{
public Task<IRequestLabelRenderReply?> OnRenderLabelAsync(RenderLabel message, CancellationToken cancellationToken)
{
IRequestLabelRenderReply reply = new LabelRendered { Key = message.Key, Payload = Render(message.Payload) };
// sent to Topics.LabelRenderResults with the request's correlation id
return Task.FromResult<IRequestLabelRenderReply?>(reply);
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarpetExpressKafka(builder.Configuration);
builder.Services.AddScoped<IRequestLabelRenderHandler, LabelFarm>();
var app = builder.Build();
app.MapPost("/shipments/{id}/delivered", async (string id, PublishShipmentEventsSender sender) =>
await sender.SendShipmentDeliveredAsync(Delivered(id)));
app.Run();
The project’s root namespace (and assembly name) is root_namespace, else --package-name or the
document’s title in PascalCase; the names of the application’s classes start with the latter
(CarpetExpress):
| File | Contents |
|---|---|
Models/*.cs |
the types of the schemas the payloads use, as apigenie-generator client csharp writes them: records with [JsonPropertyName] and DataAnnotations, enums, and unions as abstract records |
Serialization/*.cs |
the JSON context of the source generator of System.Text.Json, and the converters of unions, which read the discriminator wherever it is |
Messages/*.cs |
a record per message (Key, Headers, Payload), its headers record and header constants, with ToRecord() and FromRecord() |
Kafka/*.cs |
the settings, the handler interfaces and consumers of the received operations, the senders of the sent operations, the reply interfaces, and Add<Name>Kafka |
Infrastructure/*.cs |
the Kafka runtime (producer, consumer service, requester, headers) and the validation of messages |
<RootNamespace>.csproj, README.md |
the project |
Program.cs, appsettings.json, Handlers/*.cs |
starting points, written once and never overwritten |
- Messages. Headers are the message’s headers merged with those of its traits as AsyncAPI
does (JSON Merge Patch: a later
requiredlist replaces an earlier one). Header values are text on Kafka, so headers arestring,long,doubleorboolproperties, nullable when optional, with DataAnnotations for their length, pattern, format, values and range. Headers with a constant value, e.g. thece_specversionandce_typeof CloudEvents in binary mode, are constants (ShipmentStatusChanged.CeType), which senders write and receivers compare. Messages are validated as a whole before they are sent and after they are received. - Received operations. A handler interface per operation (
IPublishShipmentEventsHandler) has anOn<Message>Asyncmethod per message and, when there are several messages,OnUnknownAsync, which ignores records by default. Register an implementation with the dependency injection container: the operation’s consumer, aBackgroundServicein the operation’s consumer group, resolves it in a scope of its own for every record. Several messages are told apart by a payload property or a header with a distinct constant value (e.g.typeorce_type); otherwise the first message whose payload reads strictly and is valid is chosen. Records matching no message go toOnUnknownAsync. - Delivery. A record is committed once its handler returns. When the handler throws, the
consumer logs the error and handles the record again after
RetryDelay, so the records of a failing partition wait. Records that can’t be read or are invalid messages are logged as warnings and committed. Stopping the host (Ctrl+C, SIGTERM) cancels the consumers, which close and leave their groups. - Sent operations. A sender per operation (
PublishShipmentEventsSender), registered as a singleton, has aSend<Message>Asyncmethod per message that validates the message and completes once the brokers have it. Senders, requesters and repliers share one producer. - Request/reply with a reply channel: the requester’s
Request<Message>Asyncreturns the reply (one of the reply messages,IRequestLabelRenderReply), matched by the request’s correlation id, which is generated when the request has none;ReplyTimeoutbounds the wait. Each requester consumes the reply topic without a consumer group, from a minute before it started, so a reply isn’t missed and every instance of the application sees every reply. A replier’s handler returns the reply (nullfor none), which is sent to the reply topic with the request’s correlation id copied (to a header, or a payload property, as thecorrelationIdlocations say).
Settings
The AsyncApi:Kafka section of the configuration holds the settings, with the document’s values
as defaults; appsettings.json lists them, and environment variables override them as usual in
ASP.NET Core (AsyncApi__Kafka__BootstrapServers=localhost:9092):
{
"AsyncApi": {
"Kafka": {
"BootstrapServers": "kafka-eu.events.carpetexpress.example:9093",
"ClientId": "carpet-express",
"Topics": { "ShipmentEvents": "carpetexpress.shipment-events.v2" },
"Operations": {
"PublishShipmentEvents": { "GroupId": "carpet-express-publish-shipment-events", "ClientId": "carpet-express-publish-shipment-events" }
},
"ReplyTimeout": "00:00:30",
"RetryDelay": "00:00:01",
"Consumer": { "security.protocol": "SASL_SSL" },
"Producer": { "security.protocol": "SASL_SSL" }
}
}
}
BootstrapServers defaults to the host of the first Kafka server, its variables replaced by their
defaults. Group and client ids come from the groupId and clientId schemas of the Kafka
operation bindings, of the operation or its traits: their const, single enum value, default
or first example. Without a value, <package name in kebab case>-<operation> is used, and the
documentation of the setting names the binding’s pattern. The partitions, replicas and topic
configuration of channel bindings are documented on the topic settings; the generated code creates
no topics. Consumer and Producer take further librdkafka properties.
Not supported, and reported as warnings: Avro payloads and payloads of other non-JSON schema
formats (the messages are skipped, and operations left without a message too), reply addresses
given by runtime expressions, and Kafka topics with parameters. Security (SASL/SCRAM, mTLS) is not
configured: set the librdkafka properties in Consumer and Producer. With a correlation id in
the payload at a place that isn’t a string or uuid property of the payload record, the request
must carry the id and the replier’s handler must copy it.
Extensions
| Extension | Effect |
|---|---|
x-apigenie.codegen.disabled |
true on a channel or an operation leaves it out (with the operations of a channel left out) |
x-enum-varnames, x-enum-names |
names of the members of an enum, in PascalCase |
x-enum-descriptions, x-enumDescriptions |
documentation comments of the members of an enum |
The extensions can live in an overlay for this target instead of in the document, e.g.
aspnet.overlay.yaml:
overlay: 1.0.0
info:
title: ASP.NET names
version: 1.0.0
actions:
- target: $.components.schemas.OrderStatus
update:
x-enum-varnames: [Created, Paid, Shipped]
- target: $.operations.publishAudit
update:
x-apigenie:
codegen:
disabled: true
apigenie-generator server aspnet asyncapi.yaml --overlay aspnet.overlay.yaml -o out
Options
-c, –config
Path to the configuration file to use. See the “Configuration” section for more information.
–overlay OVERLAY
Path to an OpenAPI Overlay (1.0 to 1.2) applied to the input before generating, e.g. one setting the
extensions of the “Extensions” section. Can be repeated: the overlays apply in order, after those
listed under commands.generate.server_aspnet.overlay in the configuration file (with paths
relative to it). Warnings and errors about a value an overlay set point to the overlay. The overlays
apply to the AsyncAPI document bundled with the files it references.
–strict
Makes an overlay action whose target selects nothing an error (exit status 2, nothing is generated)
instead of a warning (OVERLAY:LINE:COL: action #N (TARGET) selects no nodes.).
-o, –output
Directory where the project is generated.
–package-name
Root namespace of the generated code and base of the names of the application’s classes, in
PascalCase (carpet-express gives CarpetExpress); defaults to the title of the API. Its kebab
case is the base of default group and client ids.
–package-version
Version of the project; defaults to the version of the API when it is a .NET version, else 1.0.0.
-p, –property
Applies to OpenAPI documents only; using it with an AsyncAPI document is an error.
Configuration
The options are read from the asyncapi section of the configuration of apigenie-generator server aspnet;
its other keys configure the generation from OpenAPI documents.
version: '1.0'
commands:
generate:
server_aspnet:
asyncapi:
root_namespace: Acme.Events
validation: true
program: true
perspective: application
| Option | Description |
|---|---|
root_namespace |
Root namespace and assembly name of the project (default: from --package-name or the title) |
validation |
true (default): validate messages by their DataAnnotations before sending and after receiving them |
program |
true (default): write Program.cs, appsettings.json and the handler stubs when they don’t exist |
perspective |
application (default): the described application; counterpart: its counterpart, receiving what it sends |