Skip to content

Quick Start

This guide walks through connecting to a ConvergeDB server, defining an entity kind, writing data, and subscribing to changes.

  • .NET 8.0 or later
  • A running ConvergeDB server (default port: 3727)
Terminal window
dotnet add package Convergence.Client

The Convergence.Client package includes a source generator that produces serialization code for your entity types.

Entity kinds are defined as C# structs with attributes. The source generator handles serialization.

using Convergence.Client;
[ConvergenceEntity("Player")]
public partial struct Player
{
[Field(0)] public ulong Id { get; set; }
[Field(1)] public uint Health { get; set; }
[Field(2)] public float PosX { get; set; }
[Field(3)] public float PosY { get; set; }
[Field(4, MaxLength = 32)] public string Name { get; set; }
public ReadOnlyMemory<byte> EntityId { get; init; }
}

Key points:

  • [ConvergenceEntity("Player")] names the kind on the server.
  • [Field(N)] assigns a stable ordinal to each field. These ordinals determine the wire layout and must never change.
  • EntityId is a required 32-byte property that uniquely identifies each entity within the kind.
using Convergence.Client;
// Connect to the server — by default the server allocates a source_id
// for this writer. Read client.SourceId to see what it picked.
await using var client = await ConvergenceClient.ConnectAsync(new ConvergenceOptions
{
Host = "127.0.0.1",
Port = 3727,
});
Console.WriteLine($"Connected as source #{client.SourceId}");
// Register the entity kind (safe to call on every startup)
KindHandle<Player> players = await client.RegisterKindAsync<Player>();

RegisterKindAsync uses register-if-not-exists semantics. If the kind already exists with the same schema, it returns the existing KindId. If new fields have been appended, the schema is automatically migrated.

If your deployment maps roles to known source_ids (e.g., inventory is always source 10), pass SourceId explicitly. A claim fails with ConnectClaimConflictException if another live connection currently holds the slot; claiming a disconnected source within its liveness deadline recovers the preserved entity set.

await using var client = await ConvergenceClient.ConnectAsync(new ConvergenceOptions
{
Host = "127.0.0.1",
Port = 3727,
SourceId = 10,
});

Dashboards, replicators, and other consumers that never write should open a read-only connection. Read-only clients bypass the 64-slot writer pool entirely, and the SDK rejects any write call locally before it leaves the process.

await using var reader = await ConvergenceClient.ConnectAsync(new ConvergenceOptions
{
Host = "127.0.0.1",
Port = 3727,
ReadOnly = true,
});
// reader.QueryAsync / SubscribeAsync / ResolveKindAsync work.
// reader.AssertAsync(...) throws ReadOnlyOperationException.
var player = new Player
{
EntityId = EntityId.FromGuid(Guid.NewGuid()),
Id = 42,
Health = 100,
PosX = 1.5f,
PosY = -2.3f,
Name = "Alice",
};
// Assert the entity's current state
await players.AssertAsync(player);

AssertAsync serializes the entity, buffers it, and flushes to the server. The server creates the entity if it does not exist, or updates it if it does.

Player? result = await players.QueryAsync(player.EntityId);
if (result is { } p)
{
Console.WriteLine($"Found: {p.Name} at ({p.PosX}, {p.PosY})");
}

Point reads are served entirely from the server’s in-memory entity table with no disk I/O.

await foreach (var change in players.SubscribeAsync(bootstrap: true))
{
Console.WriteLine($"{change.Type}: {change.Entity?.Name} (v{change.Version})");
}

With bootstrap: true, the server first sends a snapshot of all existing entities, then transitions to live change notifications. This ensures the subscriber starts from a complete view with no gaps.

See Subscriptions and Bootstrap for the full semantics.