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 required list replaces an earlier one). Header values are text on Kafka, so headers are string, long, double or bool properties, nullable when optional, with DataAnnotations for their length, pattern, format, values and range. Headers with a constant value, e.g. the ce_specversion and ce_type of 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 an On<Message>Async method 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, a BackgroundService in 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. type or ce_type); otherwise the first message whose payload reads strictly and is valid is chosen. Records matching no message go to OnUnknownAsync.
  • 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 a Send<Message>Async method 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>Async returns the reply (one of the reply messages, IRequestLabelRenderReply), matched by the request’s correlation id, which is generated when the request has none; ReplyTimeout bounds 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 (null for none), which is sent to the reply topic with the request’s correlation id copied (to a header, or a payload property, as the correlationId locations 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