Kafka security, Schema Registry and Avro¶
Kafka uses plaintext and System.Text.Json by default. Transport security is configured on KafkaOptions.Security.
This configuration applies to Stove's admin client, producer and observer. Exposed configuration includes Security
so the application can receive the certificate paths and selected mode through its own configuration keys.
.WithKafka(o =>
{
o.Security.Mode = KafkaSecurityMode.MutualTls; // Plaintext, Tls or MutualTls
o.Security.CaCertificateLocation = "/certificates/ca.pem";
o.Security.ClientCertificateLocation = "/certificates/client.pem";
o.Security.ClientKeyLocation = "/certificates/client-key.pem";
// Required only when Stove starts the broker:
o.Security.BrokerCertificateLocation = "/certificates/broker.pem";
o.Security.BrokerKeyLocation = "/certificates/broker-key.pem";
o.ConfigureExposedConfiguration = c =>
[
new("Kafka:BootstrapServers", c.BootstrapServers),
new("Kafka:SecurityMode", c.Security.Mode.ToString()),
new("Kafka:CaCertificateLocation", c.Security.CaCertificateLocation),
new("Kafka:ClientCertificateLocation", c.Security.ClientCertificateLocation),
new("Kafka:ClientKeyLocation", c.Security.ClientKeyLocation)
];
})
Tls requires the CA certificate; MutualTls also requires the client's certificate and private key. For an existing
broker, call o.UseExisting("broker.example:9093") and omit the broker certificate/key. The managed broker accepts
PEM certificates and PKCS#8 PEM keys; optional encrypted-key passwords are ClientKeyPassword and BrokerKeyPassword.
The broker certificate's SAN must cover the host advertised to clients (usually localhost for local Docker).
Stove keeps CA and hostname verification enabled. Security.ApplyTo(ClientConfig) is also available to native
application/test clients. Map settings to the application's actual keys; Stove does not change application DI.
Managed security configures the external listener. Broker/controller traffic and the managed registry's connection
use plaintext inside the private Docker network. This is a local test topology, with certificate enforcement on the
application/Stove connection. ConfigureContainer can customize the topology; ConfigureClient customizes native
client options, with explicit Security settings applied afterward. Selecting Plaintext overrides a transport
previously set only through ConfigureClient.
Schema Registry¶
Add StoveDotnet.SchemaRegistry and use StoveDotnet.SchemaRegistry. Register Kafka first, then its registry:
.WithKafka()
.WithSchemaRegistry(o =>
{
o.ConfigureExposedConfiguration = c => [new("SchemaRegistry:Url", c.Url)];
o.Migrations.Add(async (context, ct) =>
{
await context.Client.RegisterSchemaAsync("orders-value",
new Confluent.SchemaRegistry.Schema(schemaJson, Confluent.SchemaRegistry.SchemaType.Avro))
.WaitAsync(ct);
});
})
Kafka readiness and topic migrations finish first. Then the registry starts, checks readiness, and runs its migrations
in ascending order. All finish before the application starts. t.SchemaRegistry().Client exposes the native client;
Stove owns and disposes it. Registry cleanup runs before the client and container are disposed, including in existing mode.
Dependent systems stop before Kafka and its private network.
Use o.UseExisting("https://registry.example", runMigrations: false) for an existing registry. ConfigureClient supports
Confluent's HTTP authentication and TLS settings. A managed registry currently requires managed Kafka; existing Kafka
must be paired with an existing registry. Each registry has its own Image and ConfigureContainer hooks.
Named bindings are explicit:
.WithKafka("events")
.WithSchemaRegistry("schemas", o => { /* migrations */ }, kafkaName: "events")
Generated Avro events¶
Add StoveDotnet.Kafka.Avro and use StoveDotnet.Kafka.Avro. Apache Avro generated ISpecificRecord types can be
published and observed without a custom serde:
var stove = await StoveBuilder.Create()
.WithKafka()
.WithSchemaRegistry(o =>
{
o.Migrations.AddAvro<OrderCreated>("orders.created-value");
o.ConfigureExposedConfiguration = c => [new("SchemaRegistry:Url", c.Url)];
})
.WithKafkaAvro()
.WithAspNetCoreApplication<Program>()
.StartAsync();
await stove.Test(async t =>
{
await t.Kafka().Publish("orders.created", new OrderCreated { OrderId = "order-1" });
await t.Kafka().ShouldBePublished<OrderCreated>(m => m.Value.OrderId == "order-1");
var observed = await t.Kafka().PeekAsync<OrderCreated>("orders.created");
});
AddAvro<T> registers the generated type's schema under an explicit subject. Publishing resolves that schema's ID
through Confluent's client and emits its standard format: magic byte 0, four-byte big-endian schema ID, Avro payload.
Observation retrieves the writer schema by ID and uses Confluent's generated-type deserializer, including schema
evolution supported by that client. Keys remain UTF-8 strings; Avro applies to values. Correlation, retention limits,
consumed/failed assertions and cancellation have the same behavior as JSON.
The default subject strategy is Topic: topic-value. Auto-registration defaults to false, so missing schemas
fail publishing. Enable it explicitly or select the application's subject naming strategy:
.WithKafkaAvro(c =>
{
c.SubjectNameStrategy = Confluent.SchemaRegistry.SubjectNameStrategy.Record;
c.AutoRegisterSchemas = false;
}, kafkaName: "events", schemaRegistryName: "schemas")
For Record, register the full Avro record name; for TopicRecord, register topic-full.record.Name.
AvroKafkaSerde accepts Confluent serializer/deserializer configuration directly and can use a caller-owned registry
client. The serde does not dispose that client. Its IStoveKafkaAsyncSerde interface carries topic context and a
cancellation token. KafkaOptions.AsyncSerde, when set, takes precedence over the synchronous Serde.
Prefer PeekAsync<T> for registry-backed observation; the existing synchronous Peek<T> blocks until decoding finishes.
The built-in Avro path uses the schema-ID-prefixed wire format, rather than header-based schema identification.