Confluent.Kafka API Wrapper
The Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka package provides a drop-in replacement for the Confluent.Kafka NuGet package, enabling zero-code-change migration from Kafka to Surgewave.
Overview
This package wraps Surgewave.Client while exposing the exact same API as Confluent.Kafka. You can migrate existing Kafka applications to Surgewave by simply changing a using statement.
Migration Path
| Step |
Package |
Protocol |
Description |
| 1 |
Confluent.Kafka |
Kafka |
Original Kafka client |
| 2 |
Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka |
Auto |
Surgewave wrapper (this package) |
| 3 |
Kuestenlogik.Surgewave.Client |
Native |
Full Surgewave performance |
Installation
dotnet add package Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka
Quick Migration
Before (Confluent.Kafka)
using Confluent.Kafka;
var config = new ProducerConfig { BootstrapServers = "localhost:9092" };
using var producer = new ProducerBuilder<string, string>(config).Build();
await producer.ProduceAsync("my-topic", new Message<string, string> { Value = "Hello" });
After (Surgewave Wrapper)
using Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka; // Only change this line!
var config = new ProducerConfig { BootstrapServers = "localhost:9092" };
using var producer = new ProducerBuilder<string, string>(config).Build();
await producer.ProduceAsync("my-topic", new Message<string, string> { Value = "Hello" });
Protocol Selection
By default, the wrapper auto-detects the best protocol. You can explicitly select the protocol:
var config = new ProducerConfig
{
BootstrapServers = "localhost:9092",
SurgewaveProtocol = "surgewave" // Options: "surgewave", "kafka", "auto" (default)
};
| Protocol |
Description |
Performance |
auto |
Auto-detect (tries Surgewave first) |
Optimal |
surgewave |
Force Surgewave native protocol |
lower latency |
kafka |
Force Kafka protocol |
Kafka compatible |
Producer API
Basic Producer
using Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka;
var config = new ProducerConfig
{
BootstrapServers = "localhost:9092",
ClientId = "my-producer",
Acks = Acks.All,
EnableIdempotence = true
};
using var producer = new ProducerBuilder<string, string>(config).Build();
// Async produce
var result = await producer.ProduceAsync("orders", new Message<string, string>
{
Key = "order-123",
Value = """{"item": "widget", "qty": 5}"""
});
Console.WriteLine($"Delivered to partition {result.Partition} at offset {result.Offset}");
// Flush pending messages
producer.Flush(TimeSpan.FromSeconds(10));
var message = new Message<string, string>
{
Key = "order-123",
Value = "order data",
Headers = new Headers
{
{ "correlation-id", Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()) },
{ "source", Encoding.UTF8.GetBytes("web-api") }
}
};
await producer.ProduceAsync("orders", message);
Fire-and-Forget with Callback
producer.Produce("events", new Message<string, string>
{
Key = "event-1",
Value = "data"
}, report =>
{
if (report.Error.IsError)
Console.WriteLine($"Delivery failed: {report.Error.Reason}");
else
Console.WriteLine($"Delivered to {report.Partition}@{report.Offset}");
});
Producer with Error Handler
var producer = new ProducerBuilder<string, string>(config)
.SetErrorHandler((p, e) =>
{
Console.WriteLine($"Producer error: {e.Reason}");
if (e.IsFatal)
Environment.Exit(1);
})
.SetLogHandler((p, log) =>
{
Console.WriteLine($"[{log.Level}] {log.Message}");
})
.Build();
Consumer API
Basic Consumer
var config = new ConsumerConfig
{
BootstrapServers = "localhost:9092",
GroupId = "order-processors",
ClientId = "consumer-1",
AutoOffsetReset = AutoOffsetReset.Earliest,
EnableAutoCommit = true
};
using var consumer = new ConsumerBuilder<string, string>(config).Build();
consumer.Subscribe("orders");
while (true)
{
var result = consumer.Consume(TimeSpan.FromSeconds(1));
if (result != null && !result.IsPartitionEOF)
{
Console.WriteLine($"Received: {result.Message.Key} = {result.Message.Value}");
}
}
Consumer with Manual Commit
var config = new ConsumerConfig
{
BootstrapServers = "localhost:9092",
GroupId = "order-processors",
EnableAutoCommit = false // Manual commit
};
using var consumer = new ConsumerBuilder<string, string>(config).Build();
consumer.Subscribe("orders");
while (true)
{
var result = consumer.Consume(TimeSpan.FromSeconds(1));
if (result != null && !result.IsPartitionEOF)
{
try
{
ProcessOrder(result.Message.Value);
consumer.Commit(result); // Commit after processing
}
catch (Exception ex)
{
Console.WriteLine($"Processing failed: {ex.Message}");
// Message will be redelivered
}
}
}
Consumer with Handlers
var consumer = new ConsumerBuilder<string, string>(config)
.SetErrorHandler((c, e) =>
{
Console.WriteLine($"Consumer error: {e.Reason}");
})
.SetPartitionsAssignedHandler((c, partitions) =>
{
Console.WriteLine($"Assigned: {string.Join(", ", partitions)}");
})
.SetPartitionsRevokedHandler((c, partitions) =>
{
Console.WriteLine($"Revoked: {string.Join(", ", partitions)}");
// Commit offsets before revocation
})
.SetOffsetsCommittedHandler((c, offsets) =>
{
Console.WriteLine($"Committed {offsets.Offsets.Count} offsets");
})
.Build();
Seek and Position
// Seek to specific offset
consumer.Seek(new TopicPartitionOffset("orders", 0, 100));
// Seek to beginning
consumer.Seek(new TopicPartitionOffset("orders", 0, Offset.Beginning));
// Get current position
var position = consumer.Position(new TopicPartition("orders", 0));
Console.WriteLine($"Current position: {position}");
Admin Client
Create Topics
using Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka;
using Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka.Admin;
var config = new AdminClientConfig { BootstrapServers = "localhost:9092" };
using var admin = new AdminClientBuilder(config).Build();
await admin.CreateTopicsAsync(new[]
{
new TopicSpecification
{
Name = "new-topic",
NumPartitions = 6,
ReplicationFactor = 3,
Configs = new Dictionary<string, string>
{
["retention.ms"] = "604800000", // 7 days
["cleanup.policy"] = "delete"
}
}
});
List and Describe Topics
// Get cluster metadata
var metadata = admin.GetMetadata(TimeSpan.FromSeconds(10));
foreach (var topic in metadata.Topics)
{
Console.WriteLine($"Topic: {topic.Topic}");
foreach (var partition in topic.Partitions)
{
Console.WriteLine($" Partition {partition.PartitionId}: Leader={partition.Leader}");
}
}
// Describe specific topics
var descriptions = await admin.DescribeTopicsAsync(
TopicCollection.OfTopicNames(new[] { "orders", "events" }));
foreach (var desc in descriptions.TopicDescriptions)
{
Console.WriteLine($"{desc.Name}: {desc.Partitions.Count} partitions");
}
Delete Topics
await admin.DeleteTopicsAsync(new[] { "old-topic", "test-topic" });
Consumer Group Management
// List consumer groups
var groups = await admin.ListConsumerGroupsAsync();
foreach (var group in groups.Valid)
{
Console.WriteLine($"Group: {group.GroupId}, State: {group.State}");
}
// Describe consumer groups
var descriptions = await admin.DescribeConsumerGroupsAsync(new[] { "order-processors" });
foreach (var desc in descriptions.ConsumerGroupDescriptions)
{
Console.WriteLine($"Group {desc.GroupId}: {desc.Members.Count} members");
foreach (var member in desc.Members)
{
Console.WriteLine($" - {member.ClientId}: {member.Assignment?.TopicPartitions.Count ?? 0} partitions");
}
}
// Delete consumer group
await admin.DeleteGroupsAsync(new[] { "old-group" });
Serialization
Built-in Serializers
// String (UTF-8)
var producer = new ProducerBuilder<string, string>(config)
.SetKeySerializer(Serializers.Utf8)
.SetValueSerializer(Serializers.Utf8)
.Build();
// Byte array
var binaryProducer = new ProducerBuilder<byte[], byte[]>(config)
.SetKeySerializer(Serializers.ByteArray)
.SetValueSerializer(Serializers.ByteArray)
.Build();
// Numeric types
var intProducer = new ProducerBuilder<int, long>(config)
.SetKeySerializer(Serializers.Int32)
.SetValueSerializer(Serializers.Int64)
.Build();
// Null keys
var nullKeyProducer = new ProducerBuilder<Null, string>(config)
.SetKeySerializer(Serializers.Null)
.Build();
Built-in Deserializers
var consumer = new ConsumerBuilder<string, string>(config)
.SetKeyDeserializer(Deserializers.Utf8)
.SetValueDeserializer(Deserializers.Utf8)
.Build();
// Ignore keys
var ignoreKeyConsumer = new ConsumerBuilder<Ignore, string>(config)
.SetKeyDeserializer(Deserializers.Ignore)
.SetValueDeserializer(Deserializers.Utf8)
.Build();
Custom Serializer
public class JsonSerializer<T> : ISerializer<T>
{
public byte[]? Serialize(T data, SerializationContext context)
{
if (data is null) return null;
return JsonSerializer.SerializeToUtf8Bytes(data);
}
}
public class JsonDeserializer<T> : IDeserializer<T>
{
public T Deserialize(ReadOnlySpan<byte> data, bool isNull, SerializationContext context)
{
if (isNull || data.IsEmpty) return default!;
return JsonSerializer.Deserialize<T>(data)!;
}
}
// Usage
var producer = new ProducerBuilder<string, Order>(config)
.SetValueSerializer(new JsonSerializer<Order>())
.Build();
Configuration Reference
Producer Configuration
| Property |
Type |
Default |
Description |
BootstrapServers |
string |
- |
Broker addresses (required) |
ClientId |
string |
- |
Client identifier |
Acks |
Acks |
Leader |
Acknowledgment level |
LingerMs |
double |
5 |
Batch wait time (ms) |
BatchNumMessages |
int |
10000 |
Max batch messages |
CompressionType |
CompressionType |
None |
Compression codec |
EnableIdempotence |
bool |
false |
Enable idempotence |
TransactionalId |
string |
- |
Transaction ID |
RequestTimeoutMs |
int |
30000 |
Request timeout |
MessageTimeoutMs |
int |
300000 |
Delivery timeout |
SurgewaveProtocol |
string |
"auto" |
Protocol selection |
Consumer Configuration
| Property |
Type |
Default |
Description |
BootstrapServers |
string |
- |
Broker addresses (required) |
GroupId |
string |
- |
Consumer group ID |
ClientId |
string |
- |
Client identifier |
AutoOffsetReset |
AutoOffsetReset |
Latest |
Initial offset |
EnableAutoCommit |
bool |
true |
Auto commit offsets |
AutoCommitIntervalMs |
int |
5000 |
Commit interval |
SessionTimeoutMs |
int |
45000 |
Session timeout |
MaxPollIntervalMs |
int |
300000 |
Max poll interval |
IsolationLevel |
IsolationLevel |
ReadUncommitted |
Transaction isolation |
SurgewaveProtocol |
string |
"auto" |
Protocol selection |
Error Handling
Exception Types
try
{
await producer.ProduceAsync("topic", message);
}
catch (ProduceException<string, string> ex)
{
Console.WriteLine($"Produce failed: {ex.Error.Reason}");
Console.WriteLine($"Error code: {ex.Error.Code}");
Console.WriteLine($"Is fatal: {ex.Error.IsFatal}");
}
try
{
var result = consumer.Consume(TimeSpan.FromSeconds(10));
}
catch (ConsumeException ex)
{
Console.WriteLine($"Consume failed: {ex.Error.Reason}");
}
Error Codes
| Code |
Description |
NoError |
Success |
Local_TimedOut |
Operation timed out |
Local_QueueFull |
Producer queue full |
UnknownTopicOrPartition |
Topic doesn't exist |
RequestTimedOut |
Broker request timeout |
LeaderNotAvailable |
Partition leader unavailable |
Complete Example
using Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka;
// Producer
var producerConfig = new ProducerConfig
{
BootstrapServers = "localhost:9092",
ClientId = "order-service",
Acks = Acks.All,
SurgewaveProtocol = "surgewave" // Use Surgewave native protocol
};
using var producer = new ProducerBuilder<string, string>(producerConfig)
.SetErrorHandler((p, e) => Console.WriteLine($"Error: {e.Reason}"))
.Build();
// Produce messages
for (int i = 0; i < 100; i++)
{
var result = await producer.ProduceAsync("orders", new Message<string, string>
{
Key = $"order-{i}",
Value = $"{{\"orderId\": {i}, \"amount\": {i * 10}}}"
});
Console.WriteLine($"Produced order {i} to partition {result.Partition}");
}
producer.Flush(TimeSpan.FromSeconds(10));
// Consumer
var consumerConfig = new ConsumerConfig
{
BootstrapServers = "localhost:9092",
GroupId = "order-processors",
ClientId = "processor-1",
AutoOffsetReset = AutoOffsetReset.Earliest,
EnableAutoCommit = false,
SurgewaveProtocol = "surgewave"
};
using var consumer = new ConsumerBuilder<string, string>(consumerConfig)
.SetPartitionsAssignedHandler((c, p) =>
Console.WriteLine($"Assigned: {string.Join(", ", p)}"))
.Build();
consumer.Subscribe("orders");
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
try
{
while (!cts.Token.IsCancellationRequested)
{
var result = consumer.Consume(cts.Token);
if (result != null && !result.IsPartitionEOF)
{
Console.WriteLine($"Processing: {result.Message.Key} = {result.Message.Value}");
consumer.Commit(result);
}
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Shutting down...");
}
finally
{
consumer.Close();
}
Comparison with Native Client
| Feature |
Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka |
Kuestenlogik.Surgewave.Client |
| API Style |
Confluent.Kafka compatible |
Surgewave native |
| Migration Effort |
Zero code changes |
Code rewrite |
| Performance |
Good (auto protocol) |
Optimal |
| Learning Curve |
None (familiar API) |
New API to learn |
| Features |
Confluent.Kafka subset |
Full Surgewave features |
When to Use
Use Kuestenlogik.Surgewave.Compatibility.Confluent.Kafka when:
- Migrating existing Kafka applications
- Team is familiar with Confluent.Kafka API
- Gradual migration to Surgewave is preferred
- Need to maintain Kafka compatibility option
Use Kuestenlogik.Surgewave.Client directly when:
- Building new Surgewave applications
- Maximum performance is required
- Need Surgewave-specific features
- No Kafka compatibility requirement
Limitations
The wrapper provides full API compatibility but some advanced features depend on Surgewave support:
| Feature |
Status |
| Basic produce/consume |
Full support |
| Consumer groups |
Full support |
| Headers |
Full support |
| Transactions |
Partial (mock implementation) |
| Admin operations |
Basic support |
| Interceptors |
Not yet supported |
Next Steps