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
await using var client = await ConvergenceClient.ConnectAsync(new ConvergenceOptions
{
Host = "127.0.0.1",
Port = 3727,
SourceId = 1, // unique writer identity, 0-63
});
// 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.

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.