# server aspnet (AsyncAPI)

Source: https://apigenie.pl/techdocs/reference/cli-generate_server_aspnet_asyncapi

Generate an ASP.NET Core application of the Kafka channels of an AsyncAPI document.

## Usage

```sh
apigenie-generator server aspnet [...OPTIONS] INPUT_FILE
```

## Description

`apigenie-generator server aspnet` generates from OpenAPI documents (see
[apigenie-generator server aspnet](/techdocs/reference/cli-generate_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](https://github.com/confluentinc/confluent-kafka-dotnet)
(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.

```csharp
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`):

```json
{
  "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`:

```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
```

```sh
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.

```yaml
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 |
