This is an automated email from the ASF dual-hosted git repository.
spetz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iggy-website.git
The following commit(s) were added to refs/heads/main by this push:
new a9920c7c docs(csharp): update csharp client docs (#43)
a9920c7c is described below
commit a9920c7cd6890eb87aedc0d42da2088dd60718e8
Author: Łukasz Zborek <[email protected]>
AuthorDate: Tue Jul 28 18:39:44 2026 +0200
docs(csharp): update csharp client docs (#43)
---
content/docs/sdk/csharp/examples.mdx | 203 +++++++---
content/docs/sdk/csharp/guide.mdx | 622 +++++++++++++++++++++++++++++
content/docs/sdk/csharp/high-level-sdk.mdx | 313 +++++++++++++++
content/docs/sdk/csharp/intro.mdx | 157 +++++---
content/docs/sdk/csharp/meta.json | 2 +-
5 files changed, 1170 insertions(+), 127 deletions(-)
diff --git a/content/docs/sdk/csharp/examples.mdx
b/content/docs/sdk/csharp/examples.mdx
index a0155935..87005329 100644
--- a/content/docs/sdk/csharp/examples.mdx
+++ b/content/docs/sdk/csharp/examples.mdx
@@ -2,111 +2,186 @@
title: Examples
---
-## Basic producer
+These samples use the [High-level SDK](/docs/sdk/csharp/high-level-sdk) — the
recommended way to build producers and consumers. For the low-level, per-call
equivalents, see the [Guide](/docs/sdk/csharp/guide).
+
+## Producer
+
+A publisher that creates the stream and topic if missing, batches sends in the
background, and retries failures:
```csharp
using System.Text;
using Apache.Iggy;
using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
-using Apache.Iggy.Exceptions;
+using Apache.Iggy.Extensions;
using Apache.Iggy.Factory;
-using Apache.Iggy.Kinds;
using Apache.Iggy.Messages;
-const string StreamName = "dev";
-const string TopicName = "events";
-
-var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
{
BaseAddress = "127.0.0.1:8090",
- Protocol = Protocol.Tcp,
+ Protocol = Protocol.Tcp
});
await client.ConnectAsync();
-await client.LoginUser("iggy", "iggy");
+await client.LoginUserAsync("iggy", "iggy");
-// Create stream and topic if they don't exist
-try
-{
- await client.CreateStreamAsync(StreamName);
-}
-catch (InvalidResponseException)
-{
- Console.WriteLine("Stream already exists.");
-}
+var publisher = client.CreatePublisherBuilder(
+ Identifier.String("dev"),
+ Identifier.String("events"))
+ .CreateStreamIfNotExists("dev")
+ .CreateTopicIfNotExists("events", topicPartitionsCount: 2)
+ .WithBackgroundSending(batchSize: 100, flushInterval:
TimeSpan.FromMilliseconds(100))
+ .WithRetry(maxAttempts: 3)
+ .Build();
-try
-{
- await client.CreateTopicAsync(
- Identifier.String(StreamName),
- TopicName,
- 2,
- CompressionAlgorithm.None
- );
-}
-catch (InvalidResponseException)
-{
- Console.WriteLine("Topic already exists.");
-}
+await publisher.InitAsync();
-// Send messages
-var partitioning = Partitioning.PartitionId(0);
-for (int i = 0; i < 100; i++)
+for (var i = 0; i < 100; i++)
{
var payload = Encoding.UTF8.GetBytes($"Event #{i}");
- var messages = new List<Message> { new Message(Guid.NewGuid(), payload) };
- await client.SendMessagesAsync(
- Identifier.String(StreamName),
- Identifier.String(TopicName),
- partitioning,
- messages
- );
+ await publisher.SendMessagesAsync(new List<Message> { new(Guid.NewGuid(),
payload) });
}
+// Drain the background queue before exiting
+await publisher.WaitUntilAllSendsAsync();
+await publisher.DisposeAsync();
+
Console.WriteLine("Sent 100 messages");
```
-## Basic consumer
+## Consumer group
+
+A consumer that creates and joins a consumer group, commits offsets after each
received message, and surfaces polling errors:
```csharp
using System.Text;
using Apache.Iggy;
using Apache.Iggy.Configuration;
-using Apache.Iggy.Contracts;
+using Apache.Iggy.Consumers;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Extensions;
+using Apache.Iggy.Factory;
+using Apache.Iggy.Kinds;
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp
+});
+
+await client.ConnectAsync();
+await client.LoginUserAsync("iggy", "iggy");
+
+var consumer = client.CreateConsumerBuilder(
+ Identifier.String("dev"),
+ Identifier.String("events"),
+ Consumer.Group("event-processors"))
+ .WithConsumerGroup("event-processors", createIfNotExists: true, joinGroup:
true)
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(20)
+ .WithAutoCommitMode(AutoCommitMode.AfterReceive)
+ .SubscribeOnPollingError(e =>
+ {
+ Console.WriteLine($"Polling error: {e.Exception.Message}");
+ return Task.CompletedTask;
+ })
+ .Build();
+
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveAsync())
+{
+ var payload = Encoding.UTF8.GetString(message.Message.Payload);
+ Console.WriteLine($"Partition {message.PartitionId}, offset
{message.CurrentOffset}: {payload}");
+}
+```
+
+Run several instances of the consumer to see the group load-balance partitions
between members.
+
+## Typed messages
+
+A typed publisher/consumer pair that (de)serializes a record as JSON. Note
that the typed builders are configured via statements rather than one fluent
chain — the `With*` methods return the untyped base builder, so `Build()` must
be called on the typed builder variable (see [Typed
consumer](/docs/sdk/csharp/high-level-sdk#typed-consumer)):
+
+```csharp
+using System.Buffers;
+using System.Text.Json;
+using Apache.Iggy;
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Consumers;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Apache.Iggy.Kinds;
+using Apache.Iggy.Publishers;
-var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
{
BaseAddress = "127.0.0.1:8090",
- Protocol = Protocol.Tcp,
+ Protocol = Protocol.Tcp
});
await client.ConnectAsync();
-await client.LoginUser("iggy", "iggy");
-
-// Poll messages
-var consumer = Consumer.New(1);
-var offset = 0ul;
-uint messagesPerBatch = 100;
-
-var polledMessages = await client.PollMessagesAsync(
- Identifier.String("dev"),
- Identifier.String("events"),
- 0,
- consumer,
- PollingStrategy.Offset(offset),
- messagesPerBatch,
- false
+await client.LoginUserAsync("iggy", "iggy");
+
+// Publish typed messages
+var publisherBuilder = IggyPublisherBuilder<OrderEvent>.Create(
+ client,
+ Identifier.String("orders"),
+ Identifier.String("created"),
+ new OrderSerializer()
);
+publisherBuilder.CreateStreamIfNotExists("orders");
+publisherBuilder.CreateTopicIfNotExists("created");
+
+var publisher = publisherBuilder.Build();
+await publisher.InitAsync();
+
+await publisher.SendAsync(new OrderEvent(Guid.NewGuid(), 99.90m));
+await publisher.DisposeAsync();
+
+// Consume them
+var consumerBuilder = IggyConsumerBuilder<OrderEvent>.Create(
+ client,
+ Identifier.String("orders"),
+ Identifier.String("created"),
+ Consumer.New(1),
+ new OrderDeserializer()
+);
+consumerBuilder.WithPollingStrategy(PollingStrategy.Next());
+consumerBuilder.WithAutoCommitMode(AutoCommitMode.AfterReceive);
+
+var consumer = consumerBuilder.Build();
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveDeserializedAsync())
+{
+ if (message.Status == MessageStatus.Success)
+ {
+ Console.WriteLine($"Order {message.Data!.OrderId}, amount
{message.Data.Amount}");
+ }
+}
+
+record OrderEvent(Guid OrderId, decimal Amount);
-foreach (var message in polledMessages.Messages)
+class OrderSerializer : ISerializer<OrderEvent>
{
- var payload = Encoding.UTF8.GetString(message.Payload);
- Console.WriteLine($"Offset: {message.Header.Offset}, Payload: {payload}");
+ public void Serialize(OrderEvent data, IBufferWriter<byte> writer) =>
+ writer.Write(JsonSerializer.SerializeToUtf8Bytes(data));
+}
+
+class OrderDeserializer : IDeserializer<OrderEvent>
+{
+ public OrderEvent Deserialize(ReadOnlyMemory<byte> data) =>
+ JsonSerializer.Deserialize<OrderEvent>(data.Span)!;
}
```
-For the full source code, see the
[examples/csharp](https://github.com/apache/iggy/tree/master/examples/csharp)
directory.
+## More examples
+
+The
[examples/csharp](https://github.com/apache/iggy/tree/master/examples/csharp)
directory in the Iggy repository contains complete, runnable projects:
+
+- **Basic** / **GettingStarted** — low-level producer and consumer
+- **NewSdk** — high-level `IggyPublisher` / `IggyConsumer` (like the samples
above)
+- **MessageEnvelope** — envelope pattern (message type + JSON payload) over
the low-level client
+- **MessageHeaders** — user-defined message headers
+- **TcpTls** — TLS-encrypted TCP connection
diff --git a/content/docs/sdk/csharp/guide.mdx
b/content/docs/sdk/csharp/guide.mdx
new file mode 100644
index 00000000..b1379e9b
--- /dev/null
+++ b/content/docs/sdk/csharp/guide.mdx
@@ -0,0 +1,622 @@
+---
+title: Guide
+---
+
+This guide covers client configuration and the full `IIggyClient` API surface
— the low-level, per-call operations. For the ergonomic producer/consumer
abstractions built on top of these, see the [High-level
SDK](/docs/sdk/csharp/high-level-sdk).
+
+All examples assume you already have a connected, authenticated client (see
[Creating a client](/docs/sdk/csharp/intro#creating-a-client)).
+
+## Client configuration
+
+`IggyClientConfigurator` exposes the full set of connection options — buffer
sizes, TLS, automatic reconnection with exponential backoff, and auto-login:
+
+```csharp
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp,
+
+ // Buffer sizes (optional, default: 4096)
+ ReceiveBufferSize = 4096,
+ SendBufferSize = 4096,
+
+ // TLS/SSL configuration
+ TlsSettings = new TlsSettings
+ {
+ Enabled = true,
+ Hostname = "iggy",
+ CertificatePath = "/path/to/cert"
+ },
+
+ // Automatic reconnection with exponential backoff
+ ReconnectionSettings = new ReconnectionSettings
+ {
+ Enabled = true,
+ MaxRetries = 3, // 0 = infinite retries
+ InitialDelay = TimeSpan.FromSeconds(5),
+ MaxDelay = TimeSpan.FromSeconds(30),
+ WaitAfterReconnect = TimeSpan.FromSeconds(1),
+ UseExponentialBackoff = true,
+ BackoffMultiplier = 2.0
+ },
+
+ // Auto-login after connection
+ AutoLoginSettings = new AutoLoginSettings
+ {
+ Enabled = true,
+ Username = "iggy",
+ Password = "iggy"
+ }
+});
+
+await client.ConnectAsync();
+```
+
+With `AutoLoginSettings.Enabled = true`, the client logs in automatically once
the connection is established, so you can skip the explicit `LoginUserAsync`
call.
+
+#### `IggyClientConfigurator`
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `BaseAddress` | `string` | *required* | Server address, e.g.
`127.0.0.1:8090` |
+| `Protocol` | `Protocol` | *required* | Transport: `Protocol.Tcp` or
`Protocol.Http` |
+| `ReceiveBufferSize` | `int` | `4096` | Receive buffer size in bytes |
+| `SendBufferSize` | `int` | `4096` | Send buffer size in bytes |
+| `TlsSettings` | `TlsSettings` | disabled | TLS/SSL configuration (see below)
|
+| `ReconnectionSettings` | `ReconnectionSettings` | see below | Automatic
reconnection behavior |
+| `AutoLoginSettings` | `AutoLoginSettings` | disabled | Automatic login on
connect |
+| `LoggerFactory` | `ILoggerFactory` | `NullLoggerFactory.Instance` | Logger
factory for diagnostics (currently applied to TCP clients only) |
+| `MessageEncryptor` | `IMessageEncryptor?` | `null` | Client-side payload
encryptor (encrypts on send, decrypts on poll — see [Message
encryption](#message-encryption)) |
+| `AllowAutoCommitWithEncryptor` | `bool` | `false` | Allow auto-commit while
an encryptor is configured |
+
+#### `TlsSettings`
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `Enabled` | `bool` | `false` | Whether TLS is enabled |
+| `Hostname` | `string` | `""` | Server name for the TLS handshake |
+| `CertificatePath` | `string` | `""` | Path to the certificate (CA /
self-signed) file |
+
+#### `ReconnectionSettings`
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `Enabled` | `bool` | `false` | Enable automatic reconnection when the
connection drops |
+| `MaxRetries` | `int` | `3` | Maximum reconnection attempts (`0` = infinite) |
+| `InitialDelay` | `TimeSpan` | `5s` | Delay before the first reconnection
attempt |
+| `MaxDelay` | `TimeSpan` | `30s` | Maximum delay between attempts |
+| `WaitAfterReconnect` | `TimeSpan` | `1s` | Pause after a successful
reconnect (e.g. to rejoin a consumer group) |
+| `UseExponentialBackoff` | `bool` | `true` | Use exponential backoff for
delays |
+| `BackoffMultiplier` | `double` | `2.0` | Multiplier for exponential backoff |
+
+#### `AutoLoginSettings`
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `Enabled` | `bool` | `false` | Log in automatically once connected |
+| `Username` | `string` | `""` | Username for auto-login |
+| `Password` | `string` | `""` | Password for auto-login |
+
+## Message encryption
+
+Set `IggyClientConfigurator.MessageEncryptor` to encrypt message payloads and
user headers client-side — they are encrypted on send and decrypted on poll, so
the server only ever sees ciphertext. The built-in `AesMessageEncryptor` uses
AES-GCM (confidentiality + authenticity) and takes a 16-, 24-, or 32-byte key
for AES-128/192/256:
+
+```csharp
+using Apache.Iggy.Encryption;
+
+using var encryptor = new AesMessageEncryptor(key); // 16, 24, or 32 bytes
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp,
+ MessageEncryptor = encryptor
+});
+```
+
+For a custom scheme, implement `IMessageEncryptor`.
+
+Things to know:
+
+- The encryptor applies to **every message on the connection** — topics mixing
encrypted and plaintext messages are not supported.
+- You own the encryptor and must dispose it once the client is done with it.
+- With an encryptor configured, polling with `autoCommit: true` throws
`InvalidOperationException` by default: the server commits the offset before
the client decrypts, so a decryption failure would silently skip the whole
batch. Poll with `autoCommit: false` and store offsets after processing, or opt
in with `AllowAutoCommitWithEncryptor = true`. This guard does not affect the
high-level `IggyConsumer` commit modes.
+- On the high-level builders, `WithEncryptor(...)` is only valid when the
builder creates its own client; for an external client, set `MessageEncryptor`
on the configurator instead.
+
+## Connection events
+
+Subscribe to connection state changes — useful for reacting to reconnects
(e.g. rejoining a consumer group):
+
+```csharp
+Func<ConnectionStateChangedEventArgs, Task> handler = async args =>
+{
+ Console.WriteLine($"Current connection state: {args.CurrentState}");
+ await Task.CompletedTask;
+};
+
+client.SubscribeConnectionEvents(handler);
+
+// Later
+client.UnsubscribeConnectionEvents(handler);
+```
+
+## Identifiers
+
+Streams, topics, users, and consumer groups are referenced by an `Identifier`,
which can be either numeric or a name:
+
+```csharp
+var byId = Identifier.Numeric(0);
+var byName = Identifier.String("my-stream");
+```
+
+## Authentication
+
+### User login
+
+Begin with the root account (`iggy` / `iggy`):
+
+```csharp
+var response = await client.LoginUserAsync("iggy", "iggy");
+
+// Log out the currently authenticated user
+await client.LogoutUserAsync();
+```
+
+### Creating users
+
+Create new users with customizable permissions:
+
+```csharp
+var permissions = new Permissions
+{
+ Global = new GlobalPermissions
+ {
+ ManageServers = true,
+ ManageUsers = true,
+ ManageStreams = true,
+ ManageTopics = true,
+ PollMessages = true,
+ ReadServers = true,
+ ReadStreams = true,
+ ReadTopics = true,
+ ReadUsers = true,
+ SendMessages = true
+ }
+};
+
+await client.CreateUserAsync("test_user", "secure_password",
UserStatus.Active, permissions);
+
+var loginResponse = await client.LoginUserAsync("test_user",
"secure_password");
+```
+
+Besides `Global`, `Permissions.Streams` scopes permissions to specific streams
(keyed by numeric stream id), and each `StreamPermissions` can in turn scope
down to specific topics:
+
+```csharp
+var permissions = new Permissions
+{
+ Global = new GlobalPermissions
+ {
+ ManageServers = false,
+ ManageUsers = false,
+ ManageStreams = false,
+ ManageTopics = false,
+ PollMessages = false,
+ ReadServers = false,
+ ReadStreams = false,
+ ReadTopics = false,
+ ReadUsers = false,
+ SendMessages = false
+ },
+ Streams = new Dictionary<int, StreamPermissions>
+ {
+ [1] = new StreamPermissions
+ {
+ ManageStream = false,
+ ReadStream = true,
+ ManageTopics = false,
+ ReadTopics = true,
+ PollMessages = true,
+ SendMessages = true,
+ Topics = new Dictionary<int, TopicPermissions>
+ {
+ [1] = new TopicPermissions
+ {
+ ManageTopic = false,
+ ReadTopic = true,
+ PollMessages = true,
+ SendMessages = false
+ }
+ }
+ }
+ }
+};
+```
+
+### Managing users
+
+```csharp
+var userId = Identifier.String("test_user");
+
+// Get / list
+var user = await client.GetUserAsync(userId);
+var users = await client.GetUsersAsync();
+
+// Update name and/or status (both optional)
+await client.UpdateUserAsync(userId, userName: "renamed_user", status:
UserStatus.Inactive);
+
+// Replace permissions
+await client.UpdatePermissionsAsync(userId, permissions);
+
+// Change password
+await client.ChangePasswordAsync(userId, "secure_password", "new_password");
+
+// Delete
+await client.DeleteUserAsync(userId);
+```
+
+### Personal access tokens
+
+Create and use Personal Access Tokens (PAT) for programmatic access:
+
+```csharp
+// Create a PAT (expiry is optional; null = server default, TimeSpan.MaxValue
= never expires)
+var pat = await client.CreatePersonalAccessTokenAsync("api-token",
TimeSpan.FromHours(1));
+
+// Login with the raw token value
+await client.LoginWithPersonalAccessTokenAsync(pat!.Token);
+
+// List / delete (by name)
+var tokens = await client.GetPersonalAccessTokensAsync();
+await client.DeletePersonalAccessTokenAsync("api-token");
+```
+
+## Streams
+
+```csharp
+// Create
+await client.CreateStreamAsync("my-stream");
+
+// Get / list
+var stream = await client.GetStreamByIdAsync(Identifier.String("my-stream"));
+var streams = await client.GetStreamsAsync();
+
+// Update / purge / delete
+await client.UpdateStreamAsync(Identifier.String("my-stream"),
"renamed-stream");
+await client.PurgeStreamAsync(Identifier.String("renamed-stream"));
+await client.DeleteStreamAsync(Identifier.String("renamed-stream"));
+```
+
+## Topics
+
+Every stream contains topics that organize messages into partitions:
+
+```csharp
+var streamId = Identifier.String("my-stream");
+
+await client.CreateTopicAsync(
+ streamId,
+ name: "my-topic",
+ partitionsCount: 3,
+ compressionAlgorithm: CompressionAlgorithm.None,
+ replicationFactor: 1,
+ messageExpiry: TimeSpan.Zero, // null or TimeSpan.Zero = server default;
TimeSpan.MaxValue = never expire
+ maxTopicSize: 0 // 0 = unlimited
+);
+```
+
+`compressionAlgorithm`, `replicationFactor`, `messageExpiry`, and
`maxTopicSize` are optional. `messageExpiry` is a nullable `TimeSpan`.
+
+```csharp
+var topicId = Identifier.String("my-topic");
+
+// Get / list
+var topic = await client.GetTopicByIdAsync(streamId, topicId);
+var topics = await client.GetTopicsAsync(streamId);
+
+// Update (name required; compression, expiry, size, replication optional)
+await client.UpdateTopicAsync(streamId, topicId, "renamed-topic");
+
+// Purge (delete all messages, keep the topic) / delete
+await client.PurgeTopicAsync(streamId, Identifier.String("renamed-topic"));
+await client.DeleteTopicAsync(streamId, Identifier.String("renamed-topic"));
+```
+
+## Partitions
+
+Add partitions to or remove them from an existing topic:
+
+```csharp
+await client.CreatePartitionsAsync(streamId, topicId, partitionsCount: 2);
+await client.DeletePartitionsAsync(streamId, topicId, partitionsCount: 2);
+```
+
+## Publishing messages
+
+### Sending messages
+
+A `Message` takes an id (`Guid` or `UInt128`) and a payload:
+
+```csharp
+var streamId = Identifier.String("my-stream");
+var topicId = Identifier.String("my-topic");
+
+var messages = new List<Message>
+{
+ new(Guid.NewGuid(), "Hello, Iggy!"u8.ToArray()),
+ new(1, "Another message"u8.ToArray())
+};
+
+await client.SendMessagesAsync(
+ streamId,
+ topicId,
+ Partitioning.None(), // balanced partitioning
+ messages
+);
+```
+
+A single-message overload is also available: `SendMessagesAsync(streamId,
topicId, partitioning, message)`.
+
+To send many payloads without a `byte[]` allocation per message, build them
into a single pooled buffer with `RentedMessageBatchBuilder` — see [Publishing
with rented
batches](/docs/sdk/csharp/high-level-sdk#publishing-with-rented-batches).
+
+### Partitioning strategies
+
+Control which partition receives each message:
+
+```csharp
+// Balanced — the server selects the partition (default)
+Partitioning.None()
+
+// Send to a specific partition
+Partitioning.PartitionId(1)
+
+// Key-based routing — messages with the same key land on the same partition
+Partitioning.EntityIdString("user-123")
+Partitioning.EntityIdInt(12345)
+Partitioning.EntityIdUlong(12345)
+Partitioning.EntityIdGuid(Guid.NewGuid())
+Partitioning.EntityIdBytes(new byte[] { 1, 2, 3 })
+```
+
+### User-defined headers
+
+Add typed custom headers to messages. Build keys with `HeaderKey.FromString`
and values with the `HeaderValue.From*` factories:
+
+```csharp
+var headers = new Dictionary<HeaderKey, HeaderValue>
+{
+ { HeaderKey.FromString("correlation_id"),
HeaderValue.FromString("req-123") },
+ { HeaderKey.FromString("priority"), HeaderValue.FromInt32(1) },
+ { HeaderKey.FromString("timeout"), HeaderValue.FromInt64(5000) },
+ { HeaderKey.FromString("confidence"), HeaderValue.FromFloat(0.95f) },
+ { HeaderKey.FromString("is_urgent"), HeaderValue.FromBool(true) },
+ { HeaderKey.FromString("request_id"), HeaderValue.FromGuid(Guid.NewGuid())
}
+};
+
+var messages = new List<Message>
+{
+ new(Guid.NewGuid(), "Message with headers"u8.ToArray(), headers)
+};
+
+await client.SendMessagesAsync(streamId, topicId, Partitioning.PartitionId(1),
messages);
+```
+
+Available value factories: `FromString`, `FromBool`, `FromBytes`, `FromInt32`,
`FromInt64`, `FromInt128`, `FromUInt32`, `FromUInt64`, `FromUInt128`,
`FromFloat`, `FromDouble`, `FromGuid`.
+
+### Flushing the unsaved buffer
+
+Force a flush of the in-memory buffer to disk for a specific partition. When
`fsync` is `true`, data is both flushed and synchronized (durable):
+
+```csharp
+await client.FlushUnsavedBufferAsync(
+ Identifier.String("my-stream"),
+ Identifier.String("my-topic"),
+ partitionId: 1,
+ fsync: true
+);
+```
+
+## Consuming messages
+
+### Fetching messages
+
+Poll a batch of messages. The `partitionId` may be `null` to consume from any
partition:
+
+```csharp
+var polledMessages = await client.PollMessagesAsync(
+ streamId,
+ topicId,
+ partitionId: 0,
+ Consumer.New(1), // or Consumer.Group("my-group")
+ PollingStrategy.Next(),
+ count: 10,
+ autoCommit: true
+);
+
+foreach (var message in polledMessages.Messages)
+{
+ Console.WriteLine($"Message: {Encoding.UTF8.GetString(message.Payload)}");
+}
+```
+
+A convenience overload accepts a `MessageFetchRequest` if you prefer named
fields:
+
+```csharp
+var polledMessages = await client.PollMessagesAsync(new MessageFetchRequest
+{
+ StreamId = streamId,
+ TopicId = topicId,
+ Consumer = Consumer.New(1),
+ Count = 10,
+ PartitionId = 0,
+ PollingStrategy = PollingStrategy.Next(),
+ AutoCommit = true
+});
+```
+
+### Polling with rented buffers
+
+`PollMessagesAsync` copies each payload into its own `byte[]`. On hot paths
that allocation adds up. `PollMessagesRentedAsync` instead returns a
`PolledMessagesRental` whose payloads and raw headers are slices over a single
buffer rented from a shared pool — no per-message allocation.
+
+The rental owns that buffer, so you **must** dispose it, and the
payload/header memory is only valid until you do. Wrap it in `using` and never
hold a `Payload`/`RawUserHeaders` reference past the block:
+
+```csharp
+using var rental = await client.PollMessagesRentedAsync(
+ streamId,
+ topicId,
+ partitionId: 0,
+ Consumer.New(1),
+ PollingStrategy.Next(),
+ count: 100,
+ autoCommit: true
+);
+
+foreach (var message in rental.Messages)
+{
+ // message.Payload is ReadOnlyMemory<byte> backed by the rented buffer.
+ // Process it in place; copy out only what you need to keep.
+ var text = Encoding.UTF8.GetString(message.Payload.Span);
+ Console.WriteLine($"Offset {message.Header.Offset}: {text}");
+}
+// Buffer returns to the pool here. Payload/RawUserHeaders are invalid after
this point.
+```
+
+A `MessageFetchRequest` overload (`PollMessagesRentedAsync(request)`) is also
available.
+
+`PolledMessagesRental` (`IDisposable`) exposes:
+
+| Member | Type | Description |
+|--------|------|-------------|
+| `PartitionId` | `int` | Partition the messages came from |
+| `CurrentOffset` | `ulong` | Current offset for the partition |
+| `Messages` | `IReadOnlyList<RentedMessageResponse>` | The rented messages |
+
+Each `RentedMessageResponse`:
+
+| Member | Type | Description |
+|--------|------|-------------|
+| `Header` | `MessageHeader` | Message header (offset, timestamp, id, …) |
+| `Payload` | `ReadOnlyMemory<byte>` | Payload backed by rented memory — valid
only until the rental is disposed |
+| `RawUserHeaders` | `ReadOnlyMemory<byte>` | Raw user-header bytes backed by
rented memory |
+| `UserHeaders` | `Dictionary<HeaderKey, HeaderValue>?` | User headers, parsed
lazily and cached on first access |
+
+> **Warning:** Do not store `Payload`, `RawUserHeaders`, or a
`RentedMessageResponse` beyond the `using` scope. To retain data, copy it out
(e.g. `message.Payload.ToArray()`) before disposal.
+
+The high-level `IggyConsumer` exposes the same pooled path as an async stream
via `ReceiveRentedAsync` — see [Consuming with rented
buffers](/docs/sdk/csharp/high-level-sdk#consuming-with-rented-buffers).
+
+### Polling strategies
+
+Control where consumption starts:
+
+```csharp
+PollingStrategy.Offset(1000) // from a specific offset
+PollingStrategy.Timestamp(1699564800000000) // from a timestamp (microseconds
since epoch)
+PollingStrategy.First() // from the earliest message
+PollingStrategy.Last() // from the latest message
+PollingStrategy.Next() // from the next unread message
+```
+
+## Offset management
+
+Consumer offsets are keyed by consumer + stream + topic + partition. Note the
argument order — the `Consumer` comes first:
+
+```csharp
+var consumer = Consumer.New(1);
+var streamId = Identifier.String("my-stream");
+var topicId = Identifier.String("my-topic");
+
+// Store the current position
+await client.StoreOffsetAsync(consumer, streamId, topicId, offset: 42,
partitionId: 0);
+
+// Retrieve the stored offset
+var offsetInfo = await client.GetOffsetAsync(consumer, streamId, topicId,
partitionId: 0);
+Console.WriteLine($"Stored offset: {offsetInfo!.StoredOffset}");
+
+// Clear the stored offset
+await client.DeleteOffsetAsync(consumer, streamId, topicId, partitionId: 0);
+```
+
+## Consumer groups
+
+Consumer groups coordinate message consumption across multiple consumers,
load-balancing partitions between members.
+
+```csharp
+var streamId = Identifier.String("my-stream");
+var topicId = Identifier.String("my-topic");
+
+// Create
+await client.CreateConsumerGroupAsync(streamId, topicId, "my-consumer-group");
+
+// Inspect
+var groups = await client.GetConsumerGroupsAsync(streamId, topicId);
+var group = await client.GetConsumerGroupByIdAsync(streamId, topicId,
Identifier.String("my-consumer-group"));
+
+// Delete
+await client.DeleteConsumerGroupAsync(streamId, topicId,
Identifier.String("my-consumer-group"));
+```
+
+### Joining and leaving
+
+> **Note:** Join/Leave are TCP-only and throw `FeatureUnavailableException` on
HTTP.
+
+```csharp
+await client.JoinConsumerGroupAsync(streamId, topicId,
Identifier.String("my-consumer-group"));
+await client.LeaveConsumerGroupAsync(streamId, topicId,
Identifier.String("my-consumer-group"));
+```
+
+## System operations
+
+```csharp
+// Health check
+await client.PingAsync();
+
+// Server statistics
+var stats = await client.GetStatsAsync();
+
+// Cluster metadata and node information
+var metadata = await client.GetClusterMetadataAsync();
+
+// Connected clients
+var clients = await client.GetClientsAsync();
+var clientById = await client.GetClientByIdAsync(clientId: 1);
+var currentClient = await client.GetMeAsync(); // TCP-only
+```
+
+### Snapshots
+
+Capture a system snapshot as a compressed archive:
+
+```csharp
+var snapshotBytes = await client.GetSnapshotAsync(
+ SnapshotCompression.Zstd,
+ new List<SystemSnapshotType>
+ {
+ SystemSnapshotType.ServerLogs,
+ SystemSnapshotType.ServerConfig,
+ SystemSnapshotType.ResourceUsage
+ }
+);
+
+// Or capture everything
+var fullSnapshot = await client.GetSnapshotAsync(
+ SnapshotCompression.Deflated,
+ new List<SystemSnapshotType> { SystemSnapshotType.All }
+);
+```
+
+Compression methods: `Stored`, `Deflated`, `Bzip2`, `Zstd`, `Lzma`, `Xz`.
+Snapshot types: `FilesystemOverview`, `ProcessList`, `ResourceUsage`, `Test`,
`ServerLogs`, `ServerConfig`, `All`.
+
+### Segment management
+
+Delete the last N segments from a partition:
+
+> **Note:** TCP-only — throws `FeatureUnavailableException` on HTTP.
+
+```csharp
+await client.DeleteSegmentsAsync(
+ Identifier.String("my-stream"),
+ Identifier.String("my-topic"),
+ partitionId: 1,
+ segmentsCount: 2
+);
+```
diff --git a/content/docs/sdk/csharp/high-level-sdk.mdx
b/content/docs/sdk/csharp/high-level-sdk.mdx
new file mode 100644
index 00000000..04eb51c8
--- /dev/null
+++ b/content/docs/sdk/csharp/high-level-sdk.mdx
@@ -0,0 +1,313 @@
+---
+title: High-level SDK
+---
+
+The per-call [`IIggyClient`](/docs/sdk/csharp/guide) API is explicit but
verbose — you manage partitioning, batching, retries, and offsets yourself. The
high-level `IggyPublisher` and `IggyConsumer` wrap that surface with fluent
builders that handle:
+
+- Background sending with buffering, batching, and retries
+- Automatic stream/topic/consumer-group creation
+- Automatic offset commits (on each poll, after each received message, or
manual)
+- Consuming a topic as an async stream (`IAsyncEnumerable`)
+- Typed message (de)serialization
+- Sending and receiving over rented, pooled buffers — no per-message allocation
+
+Both are built from an existing connected client via
`IggyPublisherBuilder.Create(...)` / `IggyConsumerBuilder.Create(...)`, or
equivalently via the `client.CreatePublisherBuilder(...)` /
`client.CreateConsumerBuilder(...)` extension methods from
`Apache.Iggy.Extensions`.
+
+## IggyPublisher
+
+Configure a publisher, initialize it, then send. `InitAsync` validates (and
optionally creates) the stream and topic:
+
+```csharp
+using Apache.Iggy;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Publishers;
+
+var publisher = IggyPublisherBuilder.Create(
+ client,
+ Identifier.String("my-stream"),
+ Identifier.String("my-topic")
+)
+.WithPartitioning(Partitioning.None())
+.WithBackgroundSending(enabled: true, batchSize: 100)
+.WithRetry(maxAttempts: 3)
+.Build();
+
+await publisher.InitAsync();
+
+var messages = new List<Message>
+{
+ new(Guid.NewGuid(), "Message 1"u8.ToArray()),
+ new(0, "Message 2"u8.ToArray())
+};
+
+await publisher.SendMessagesAsync(messages);
+
+// Drain the background queue, then dispose
+await publisher.WaitUntilAllSendsAsync();
+await publisher.DisposeAsync();
+```
+
+### Publisher builder options
+
+| Method | Description |
+|--------|-------------|
+| `WithConnection(protocol, address, login, password, receiveBufferSize =
4096, sendBufferSize = 4096, reconnectionSettings = null)` | Connection
settings — only used when the builder creates its own client (i.e. the
`Create(streamId, topicId)` overload, without an existing client) |
+| `WithPartitioning(partitioning)` | Routing strategy for produced messages
(default: balanced) |
+| `CreateStreamIfNotExists(name)` | Auto-create the stream on `InitAsync` if
missing |
+| `CreateTopicIfNotExists(name, topicPartitionsCount = 1, compressionAlgorithm
= None, replicationFactor = null, messageExpiry = TimeSpan.Zero, maxTopicSize =
0)` | Auto-create the topic on `InitAsync` if missing |
+| `WithRetry(enabled = true, maxAttempts = 3, initialDelay = 100ms, maxDelay =
10s, backoffMultiplier = 2.0)` | Retry failed sends with exponential backoff |
+| `WithBackgroundSending(enabled = true, queueCapacity = 10000, batchSize =
100, flushInterval = 100ms, disposalTimeout = 5s)` | Queue and flush sends in
the background for higher throughput |
+| `WithEncryptor(encryptor)` | Client-side payload encryption. Only valid on a
builder-created client — for an external client, set
`IggyClientConfigurator.MessageEncryptor` instead |
+| `SubscribeOnBackgroundError(handler)` | Observe background-processing errors
(only fires when background sending is enabled) |
+| `SubscribeOnMessageBatchFailed(handler)` | Fires when a batch fails after
all retries are exhausted (only with background sending — direct sends throw to
the caller instead) |
+| `WithLogger(loggerFactory)` | Logger factory for diagnostics |
+
+**`WithRetry` parameters**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `enabled` | `bool` | `true` | Whether retry is enabled |
+| `maxAttempts` | `int` | `3` | Maximum retry attempts |
+| `initialDelay` | `TimeSpan?` | `100ms` | Delay before the first retry |
+| `maxDelay` | `TimeSpan?` | `10s` | Maximum delay between retries |
+| `backoffMultiplier` | `double` | `2.0` | Exponential backoff multiplier |
+
+**`WithBackgroundSending` parameters**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `enabled` | `bool` | `true` | Whether background sending is enabled |
+| `queueCapacity` | `int` | `10000` | Max queued send calls (one slot per
`SendMessagesAsync` call, regardless of batch size) |
+| `batchSize` | `int` | `100` | Messages sent per batch |
+| `flushInterval` | `TimeSpan?` | `100ms` | Interval at which pending messages
are flushed |
+| `disposalTimeout` | `TimeSpan?` | `5s` | How long `DisposeAsync` waits for
the background processor to drain |
+
+A batch also flushes once its accumulated payload reaches
`IggyPublisherConfig.BackgroundMaxBatchBytes` (default 256 KB, `0` disables the
byte gate) — whichever of `batchSize` or the byte limit is hit first. This knob
has no builder method; set it on the config directly if needed.
+
+### Publishing with rented batches
+
+`SendMessagesAsync` takes messages whose payloads each live in their own
`byte[]`. On hot paths, build a `RentedMessageBatch` instead — every payload is
written into a single buffer rented from the shared array pool — and hand it to
`SendAsync`:
+
+```csharp
+using System.Text.Json;
+using Apache.Iggy.Messages;
+
+using var builder = new RentedMessageBatchBuilder(sizeHint: 4096);
+
+foreach (var evt in events)
+{
+ // Serialize straight into the pooled buffer (static lambda + state = no
closure allocation)
+ builder.Add(evt, static (e, writer) =>
+ {
+ using var json = new Utf8JsonWriter(writer);
+ JsonSerializer.Serialize(json, e);
+ }, Guid.NewGuid());
+}
+
+var batch = builder.Build();
+await publisher.SendAsync(batch);
+```
+
+`RentedMessageBatchBuilder`:
+
+| Member | Description |
+|--------|-------------|
+| `new RentedMessageBatchBuilder(sizeHint = 1024)` | Initial capacity hint
(bytes) for the rented buffer |
+| `Add(payload, id = null, userHeaders = null)` | Copies a
`ReadOnlySpan<byte>` payload into the shared buffer |
+| `Add(state, writePayload, id = null, userHeaders = null)` | Writes the
payload via an `Action<TState, IBufferWriter<byte>>` callback directly into the
buffer |
+| `Count` | Number of payloads added so far |
+| `Build()` | Materializes the `RentedMessageBatch`; the builder must not be
used afterwards |
+
+A `null` message id sends the message with id 0, letting the server assign one.
+
+Ownership rules:
+
+- `publisher.SendAsync(batch)` **takes ownership** — do not dispose the batch
yourself. Its buffer returns to the pool once the batch is sent (immediately,
or after the background flush).
+- For a direct low-level send, pass `batch.Messages` to
`client.SendMessagesAsync(...)` yourself, `await` it, then dispose the batch
(`using var batch = builder.Build()`).
+- After disposal, `batch.Messages` and every payload's `Span` throw
`ObjectDisposedException` instead of reading recycled pool memory.
+- Dispose the builder only when abandoning it before `Build()`; a `using` on
the builder is safe either way (it becomes a no-op after `Build()`).
+
+### Typed publisher
+
+For automatic object serialization, use `IggyPublisherBuilder<T>` with an
`ISerializer<T>`:
+
+```csharp
+class OrderSerializer : ISerializer<Order>
+{
+ public void Serialize(Order data, IBufferWriter<byte> writer) =>
+ writer.Write(JsonSerializer.SerializeToUtf8Bytes(data));
+}
+
+var publisher = IggyPublisherBuilder<Order>.Create(
+ client,
+ Identifier.String("orders-stream"),
+ Identifier.String("orders-topic"),
+ new OrderSerializer()
+).Build();
+
+await publisher.InitAsync();
+await publisher.SendAsync(new List<Order> { /* ... */ });
+```
+
+Besides the collection overload, `SendAsync` also accepts a single item
(`SendAsync(order, messageId: null, userHeaders: null)`) or a collection of
`(data, messageId, userHeaders)` tuples when you need per-message ids or
headers.
+
+For JSON you don't need a custom serializer — the built-in
`SystemTextJsonSerializer<T>` (in `Apache.Iggy.Publishers`) writes
System.Text.Json output directly into the send buffer, with optional
`JsonSerializerOptions`:
+
+```csharp
+var publisher = IggyPublisherBuilder<Order>.Create(
+ client,
+ Identifier.String("orders-stream"),
+ Identifier.String("orders-topic"),
+ new SystemTextJsonSerializer<Order>()
+).Build();
+```
+
+There is no built-in deserializer counterpart — consumers implement
`IDeserializer<T>` themselves (see [Typed consumer](#typed-consumer)).
+
+## IggyConsumer
+
+Configure a consumer, initialize it, then iterate. `ReceiveAsync` returns an
`IAsyncEnumerable<ReceivedMessage>`:
+
+```csharp
+using System.Text;
+using Apache.Iggy;
+using Apache.Iggy.Consumers;
+using Apache.Iggy.Kinds;
+
+var consumer = IggyConsumerBuilder.Create(
+ client,
+ Identifier.String("my-stream"),
+ Identifier.String("my-topic"),
+ Consumer.New(1)
+)
+.WithPollingStrategy(PollingStrategy.Next())
+.WithBatchSize(10)
+.WithAutoCommitMode(AutoCommitMode.Auto)
+.Build();
+
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveAsync())
+{
+ var payload = Encoding.UTF8.GetString(message.Message.Payload);
+ Console.WriteLine($"Offset {message.CurrentOffset}: {payload}");
+}
+```
+
+### Consumer builder options
+
+| Method | Description |
+|--------|-------------|
+| `WithConnection(protocol, address, login, password, receiveBufferSize =
4096, sendBufferSize = 4096, reconnectionSettings = null)` | Connection
settings — only used when the builder creates its own client |
+| `WithPartitionId(partitionId)` | Consume from a specific partition |
+| `WithPollingStrategy(pollingStrategy)` | Where to start consuming (default:
`Offset(0)`). An `Offset(...)` strategy is advanced client-side after each
poll; other strategies (e.g. `Next()`) are sent as-is and rely on server-side
offset tracking |
+| `WithBatchSize(batchSize)` | Messages fetched per poll (default: `100`) |
+| `WithAutoCommitMode(mode)` | Offset auto-commit behavior (see below) |
+| `WithConsumerGroup(groupName, createIfNotExists = true, joinGroup = true)` |
Create and/or join a consumer group |
+| `WithPollingInterval(interval)` | Delay between polls to throttle server
requests (default: `100ms`); `TimeSpan.Zero` disables throttling |
+| `WithEncryptor(encryptor)` | Client-side payload decryption. Only valid on a
builder-created client — for an external client, set
`IggyClientConfigurator.MessageEncryptor` instead. Cannot be combined with
`AutoCommitMode.Auto` (the builder throws — a decryption failure would silently
skip an already-committed batch) |
+| `SubscribeOnPollingError(handler)` | Observe polling errors |
+| `WithLogger(loggerFactory)` | Logger factory for diagnostics |
+
+Auto-commit modes (`AutoCommitMode`):
+
+| Mode | Description |
+|------|-------------|
+| `Auto` | Commit the offset while polling |
+| `AfterReceive` | Commit after each message is received |
+| `Disabled` | Commit manually |
+
+### Manual offset control
+
+With `AutoCommitMode.Disabled`, commit offsets yourself via the consumer's
`StoreOffsetAsync`:
+
+```csharp
+await foreach (var message in consumer.ReceiveAsync())
+{
+ Process(message);
+ await consumer.StoreOffsetAsync(message.CurrentOffset,
message.PartitionId);
+}
+```
+
+`StoreOffsetAsync(offset, partitionId, resetLastPolled = false)` stores the
offset for a partition; pass `resetLastPolled: true` to also move the
consumer's cached last-polled position so the next poll resumes past the stored
offset. `DeleteOffsetAsync(partitionId)` clears the stored offset.
+
+### Consumer groups
+
+Pass a `Consumer.Group(...)` and let the builder create and join the group for
load-balanced consumption:
+
+```csharp
+var consumer = IggyConsumerBuilder.Create(
+ client,
+ Identifier.String("my-stream"),
+ Identifier.String("my-topic"),
+ Consumer.Group("my-group")
+)
+.WithConsumerGroup("my-group", createIfNotExists: true, joinGroup: true)
+.WithPollingStrategy(PollingStrategy.Next())
+.WithAutoCommitMode(AutoCommitMode.AfterReceive)
+.Build();
+
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveAsync())
+{
+ var payload = Encoding.UTF8.GetString(message.Message.Payload);
+ Console.WriteLine($"Partition {message.PartitionId}: {payload}");
+}
+
+await consumer.DisposeAsync();
+```
+
+### Consuming with rented buffers
+
+`ReceiveAsync` copies each payload into its own `byte[]`. `ReceiveRentedAsync`
is the high-level counterpart of the low-level [rented poll
API](/docs/sdk/csharp/guide#polling-with-rented-buffers): payloads are slices
of a pooled buffer shared by all messages from the same poll. Each yielded
`ReceivedRentedMessage` **must be disposed** — the buffer returns to the pool
once the last message of its batch is disposed:
+
+```csharp
+await foreach (var message in consumer.ReceiveRentedAsync())
+{
+ using (message)
+ {
+ var text = Encoding.UTF8.GetString(message.Message.Payload.Span);
+ Console.WriteLine($"Offset {message.CurrentOffset}: {text}");
+ }
+}
+```
+
+`ReceivedRentedMessage` (`IDisposable`) exposes `Message` (a
[`RentedMessageResponse`](/docs/sdk/csharp/guide#polling-with-rented-buffers)),
`CurrentOffset`, `PartitionId`, `Status`, and `Error`. This path performs no
deserialization, so `Status` is always `Success`. Payload and raw-header memory
are only valid until the message is disposed — copy out anything you need to
keep (e.g. `message.Message.Payload.ToArray()`).
+
+Auto-commit modes apply exactly as with `ReceiveAsync`. Forgetting to dispose
a message never corrupts data — the batch buffer simply isn't returned to the
pool and is reclaimed by the GC as an ordinary allocation.
+
+### Typed consumer
+
+For automatic deserialization, use `IggyConsumerBuilder<T>` with an
`IDeserializer<T>` and iterate with `ReceiveDeserializedAsync`. Each
`ReceivedMessage<T>` carries a `Status` you should check:
+
+```csharp
+class OrderDeserializer : IDeserializer<OrderEvent>
+{
+ public OrderEvent Deserialize(ReadOnlyMemory<byte> data) =>
+ JsonSerializer.Deserialize<OrderEvent>(data.Span)!;
+}
+
+var builder = IggyConsumerBuilder<OrderEvent>.Create(
+ client,
+ Identifier.String("orders-stream"),
+ Identifier.String("orders-topic"),
+ Consumer.Group("order-processors"),
+ new OrderDeserializer()
+);
+builder.WithAutoCommitMode(AutoCommitMode.AfterReceive);
+
+var consumer = builder.Build();
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveDeserializedAsync())
+{
+ if (message.Status == MessageStatus.Success)
+ {
+ Console.WriteLine($"Order: {message.Data?.OrderId}");
+ }
+}
+```
+
+> **Note:** On the typed builders, the fluent `With*` methods are inherited
from the untyped base builder and return the base type, while the typed
`Build()` hides the base one. Chaining
`Create(...).WithAutoCommitMode(...).Build()` therefore resolves to the base
`Build()` and returns an untyped consumer/publisher. Keep the typed builder in
a variable (as above) and call `Build()` on it — the `With*` calls mutate the
builder, so their return value can be ignored.
diff --git a/content/docs/sdk/csharp/intro.mdx
b/content/docs/sdk/csharp/intro.mdx
index 441d8d53..ab6761bb 100644
--- a/content/docs/sdk/csharp/intro.mdx
+++ b/content/docs/sdk/csharp/intro.mdx
@@ -2,7 +2,9 @@
title: C# SDK
---
-The Iggy SDK for C# is a library that allows you to interact with the Iggy API
from your .NET applications. It supports TCP and HTTP transports. The package
is available on [NuGet](https://www.nuget.org/packages/Apache.Iggy/) and the
source code can be found on
[GitHub](https://github.com/apache/iggy/tree/master/foreign/csharp).
+The Iggy SDK for C# is a modern, async-first client library for interacting
with an Iggy message streaming server from your .NET applications. It supports
TCP and HTTP transports. The package is available on
[NuGet](https://www.nuget.org/packages/Apache.Iggy/) and the source code lives
on [GitHub](https://github.com/apache/iggy/tree/master/foreign/csharp).
+
+The SDK is built around the `IIggyClient` interface, which aggregates every
feature (publishing, consuming, stream/topic management, users, offsets,
consumer groups, and system operations). For the low-level per-call API and the
full configuration reference, see the [Guide](/docs/sdk/csharp/guide). For the
ergonomic, batteries-included producer/consumer abstractions, see the
[High-level SDK](/docs/sdk/csharp/high-level-sdk).
## Installation
@@ -10,8 +12,60 @@ The Iggy SDK for C# is a library that allows you to interact
with the Iggy API f
dotnet add package Apache.Iggy
```
+## Supported protocols
+
+The SDK supports two transport protocols:
+
+- **TCP** — binary protocol for optimal performance and lower latency
(recommended)
+- **HTTP** — RESTful JSON API for stateless operations
+
+Some operations are TCP-only and throw `FeatureUnavailableException` on HTTP:
joining/leaving a consumer group, `GetMeAsync`, and `DeleteSegmentsAsync`.
+
+## Creating a client
+
+Create a client with `IggyClientFactory.CreateClient`, then call
`ConnectAsync`:
+
+```csharp
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Factory;
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp
+});
+
+await client.ConnectAsync();
+await client.LoginUserAsync("iggy", "iggy");
+```
+
+Optionally, provide an `ILoggerFactory` for diagnostics (defaults to
`NullLoggerFactory.Instance`):
+
+```csharp
+using Microsoft.Extensions.Logging;
+
+var loggerFactory = LoggerFactory.Create(builder =>
+{
+ builder
+ .AddFilter("Apache.Iggy", LogLevel.Information)
+ .AddConsole();
+});
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
+```
+
+`IggyClientConfigurator` also exposes buffer sizes, TLS, automatic
reconnection with exponential backoff, auto-login (so you can skip the explicit
`LoginUserAsync` call), and client-side message encryption. See [Client
configuration](/docs/sdk/csharp/guide#client-configuration) for the full
reference.
+
## Quick start
+These samples use the [High-level SDK](/docs/sdk/csharp/high-level-sdk) — the
recommended way to build producers and consumers. For the equivalent low-level,
per-call flow, see the [Guide](/docs/sdk/csharp/guide).
+
### Producer
```csharp
@@ -19,42 +73,35 @@ using System.Text;
using Apache.Iggy;
using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
+using Apache.Iggy.Extensions;
using Apache.Iggy.Factory;
-using Apache.Iggy.Kinds;
using Apache.Iggy.Messages;
-const string StreamName = "sample-stream";
-const string TopicName = "sample-topic";
-
-var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
{
BaseAddress = "127.0.0.1:8090",
- Protocol = Protocol.Tcp,
+ Protocol = Protocol.Tcp
});
await client.ConnectAsync();
-await client.LoginUser("iggy", "iggy");
-
-await client.CreateStreamAsync(StreamName);
-await client.CreateTopicAsync(
- Identifier.String(StreamName),
- TopicName,
- 1,
- CompressionAlgorithm.None
-);
-
-var partitioning = Partitioning.PartitionId(0);
-for (int i = 0; i < 10; i++)
+await client.LoginUserAsync("iggy", "iggy");
+
+var publisher = client.CreatePublisherBuilder(
+ Identifier.String("sample-stream"),
+ Identifier.String("sample-topic"))
+ .CreateStreamIfNotExists("sample-stream")
+ .CreateTopicIfNotExists("sample-topic")
+ .Build();
+
+await publisher.InitAsync();
+
+for (var i = 0; i < 10; i++)
{
var payload = Encoding.UTF8.GetBytes($"message-{i}");
- var messages = new List<Message> { new Message(Guid.NewGuid(), payload) };
- await client.SendMessagesAsync(
- Identifier.String(StreamName),
- Identifier.String(TopicName),
- partitioning,
- messages
- );
+ await publisher.SendMessagesAsync(new List<Message> { new(Guid.NewGuid(),
payload) });
}
+
+await publisher.DisposeAsync();
```
### Consumer
@@ -63,56 +110,42 @@ for (int i = 0; i < 10; i++)
using System.Text;
using Apache.Iggy;
using Apache.Iggy.Configuration;
-using Apache.Iggy.Contracts;
+using Apache.Iggy.Consumers;
using Apache.Iggy.Enums;
+using Apache.Iggy.Extensions;
using Apache.Iggy.Factory;
using Apache.Iggy.Kinds;
-var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
{
BaseAddress = "127.0.0.1:8090",
- Protocol = Protocol.Tcp,
+ Protocol = Protocol.Tcp
});
await client.ConnectAsync();
-await client.LoginUser("iggy", "iggy");
+await client.LoginUserAsync("iggy", "iggy");
-var consumer = Consumer.New(1);
-var offset = 0ul;
-uint messagesPerBatch = 10;
-
-while (true)
-{
- var polledMessages = await client.PollMessagesAsync(
+var consumer = client.CreateConsumerBuilder(
Identifier.String("sample-stream"),
Identifier.String("sample-topic"),
- 0,
- consumer,
- PollingStrategy.Offset(offset),
- messagesPerBatch,
- false
- );
-
- if (!polledMessages.Messages.Any()) break;
-
- offset += (ulong)polledMessages.Messages.Count;
- foreach (var message in polledMessages.Messages)
- {
- var payload = Encoding.UTF8.GetString(message.Payload);
- Console.WriteLine($"Offset: {message.Header.Offset}, Payload:
{payload}");
- }
+ Consumer.New(1))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithAutoCommitMode(AutoCommitMode.AfterReceive)
+ .Build();
+
+await consumer.InitAsync();
+
+await foreach (var message in consumer.ReceiveAsync())
+{
+ var payload = Encoding.UTF8.GetString(message.Message.Payload);
+ Console.WriteLine($"Offset {message.CurrentOffset}: {payload}");
}
```
-## Examples
-
-Working examples are available in the
[examples/csharp](https://github.com/apache/iggy/tree/master/examples/csharp)
directory. The following example sets are included:
+`ReceiveAsync` polls indefinitely — pass a `CancellationToken` or `break` out
of the loop to stop.
-- **GettingStarted** - basic producer and consumer
-- **Basic** - producer and consumer with settings
-- **NewSdk** - new high-level SDK API patterns
-- **MessageEnvelope** - working with message envelopes
-- **MessageHeaders** - custom message headers
-- **TcpTls** - TLS-encrypted TCP connections
+## Next steps
-The solution can be opened with Visual Studio or built with `dotnet build`.
+- [Guide](/docs/sdk/csharp/guide) — client configuration reference and the
full API surface: auth, streams, topics, partitions, publishing, consuming,
offsets, consumer groups, system operations
+- [High-level SDK](/docs/sdk/csharp/high-level-sdk) — `IggyPublisher` /
`IggyConsumer` with background sending, retries, auto-commit, typed
(de)serialization, and pooled (rented) buffers for allocation-free hot paths
+- [Examples](/docs/sdk/csharp/examples) — producer, consumer-group, and
typed-message samples, plus links to runnable projects
diff --git a/content/docs/sdk/csharp/meta.json
b/content/docs/sdk/csharp/meta.json
index 6c1e2caa..358882d9 100644
--- a/content/docs/sdk/csharp/meta.json
+++ b/content/docs/sdk/csharp/meta.json
@@ -1 +1 @@
-{"title": "C#", "pages": ["intro", "examples"]}
+{"title": "C#", "pages": ["intro", "guide", "high-level-sdk", "examples"]}