Skip to main content

.NET Driver

CamusDB ships an ADO.NET provider for direct access from .NET applications. The package name is CamusDB.Client.

It targets net8.0 and net9.0.

Install

dotnet add package CamusDB.Client

Connection String

Create a CamusConnection with a connection string containing:

  • Endpoint: the base CamusDB HTTP endpoint
  • Database: the database name to use
using CamusDB.Client;

CamusConnectionStringBuilder builder =
new("Endpoint=http://localhost:5095;Database=test");

await using CamusConnection connection = new(builder);
await connection.OpenAsync();

Supported keys:

KeyRequiredDescription
EndpointYesBase URL for the CamusDB node.
DatabaseYesDatabase name sent on requests.

Endpoint can also be a comma-separated pool:

Endpoint=http://localhost:5095,http://localhost:5096,http://localhost:5097;Database=test

The client uses round-robin routing across endpoints. If one endpoint becomes unreachable, it is marked unhealthy and skipped by later requests that use the same connection-string builder.

Open A Connection

await using CamusConnection connection =
new(new CamusConnectionStringBuilder(
"Endpoint=http://localhost:5095;Database=test"));

await connection.OpenAsync();

ChangeDatabase("otherdb") updates the target database on the connection.

Opening a connection does not create the database. Create databases explicitly before running table DDL, DML, or queries:

await using CamusCommand createDb = connection.CreateCamusCommand(
"CREATE DATABASE IF NOT EXISTS test");

await createDb.ExecuteDDLAsync();

Ping

Use a ping command to verify connectivity:

await using CamusCommand ping = connection.CreatePingCommand();
int result = await ping.ExecuteNonQueryAsync();

Run DDL

Use CreateCamusCommand(...) for SQL statements:

await using CamusCommand ddl = connection.CreateCamusCommand("""
CREATE TABLE robots (
id OID PRIMARY KEY NOT NULL,
name STRING NOT NULL,
kind STRING,
year INT64,
price FLOAT64,
enabled BOOL
)
""");

bool created = await ddl.ExecuteDDLAsync();

Insert Rows

For inserts, you can either use the insert helper or parameterized SQL.

Insert helper

using CamusDB.Core.Util.ObjectIds;

await using CamusInsertCommand insert = connection.CreateInsertCommand("robots");

insert.Parameters.Add("id", ColumnType.Id, CamusObjectIdGenerator.Generate());
insert.Parameters.Add("name", ColumnType.String, "T-800");
insert.Parameters.Add("kind", ColumnType.String, "cyborg");
insert.Parameters.Add("year", ColumnType.Integer64, 1984);
insert.Parameters.Add("price", ColumnType.Float64, 10.0);
insert.Parameters.Add("enabled", ColumnType.Bool, true);

int inserted = await insert.ExecuteNonQueryAsync();

Parameterized SQL

await using CamusCommand insert = connection.CreateCamusCommand("""
INSERT INTO robots (id, name, year, kind, price, enabled)
VALUES (GEN_ID(), @name, @year, @kind, @price, @enabled)
""");

insert.Parameters.Add("@name", ColumnType.String, "R2-D2");
insert.Parameters.Add("@year", ColumnType.Integer64, 1977);
insert.Parameters.Add("@kind", ColumnType.String, "mechanical");
insert.Parameters.Add("@price", ColumnType.Float64, 25.5);
insert.Parameters.Add("@enabled", ColumnType.Bool, true);

int inserted = await insert.ExecuteNonQueryAsync();

Query Rows

Use ExecuteReaderAsync() to stream result rows:

await using CamusCommand select = connection.CreateSelectCommand(
"SELECT id, name, year FROM robots WHERE year = @year");

select.Parameters.Add("@year", ColumnType.Integer64, 1977);

await using CamusDataReader reader = await select.ExecuteReaderAsync();

while (await reader.ReadAsync())
{
string id = reader.GetString(0);
string name = reader.GetString(1);
long year = reader.GetInt64(2);
}

The reader exposes standard typed getters such as:

  • GetString
  • GetBoolean
  • GetInt16 / GetInt32 / GetInt64
  • GetFloat / GetDouble
  • GetGuid
  • IsDBNull

Parameters

Parameters are input-only. Supported value mappings include:

Camus typeTypical .NET values
ColumnType.Idstring, Guid, Camus object id values
ColumnType.Stringstring
ColumnType.Integer64short, int, long, other integer-convertible values
ColumnType.Float64float, double
ColumnType.Boolbool
ColumnType.Nullnull, DBNull.Value

Examples:

command.Parameters.Add("@id", ColumnType.Id, Guid.NewGuid());
command.Parameters.Add("@count", ColumnType.Integer64, 5);
command.Parameters.Add("@price", ColumnType.Float64, 19.99);
command.Parameters.Add("@note", ColumnType.Null, null);

Transactions

CamusDB transactions are exposed through BeginTransactionAsync():

CamusTransaction tx = await connection.BeginTransactionAsync();

await using CamusCommand insert = connection.CreateCamusCommand("""
INSERT INTO robots (id, name, year)
VALUES (GEN_ID(), @name, @year)
""");

insert.Transaction = tx;
insert.Parameters.Add("@name", ColumnType.String, "HAL 9000");
insert.Parameters.Add("@year", ColumnType.Integer64, 1968);

await insert.ExecuteNonQueryAsync();
await tx.CommitAsync();

Use await tx.RollbackAsync() to abort the transaction.

The driver only accepts IsolationLevel.Serializable and IsolationLevel.Unspecified, which matches CamusDB's transaction model. Unspecified transactions inherit CamusDB's server default, which is Serializable.

The current ADO.NET driver does not expose a Read Committed transaction option. Use SQL or the HTTP API directly if you need to opt a transaction down to Read Committed.

Serializable Retries

Serializable is the default isolation level in CamusDB. When two serializable read-write transactions conflict, one transaction is aborted and the whole unit of work must be replayed from the beginning.

The client package includes SerializableRetryHelper for that retry contract. Only these CamusDB error codes are treated as retryable:

CodeNameMeaning
CADB0502TransactionConflictA lock conflict aborted the transaction.
CADB0504TransactionMustRetryA transient routing or leader-transition condition exhausted the commit retry budget.
CADB0505TransactionLifetimeExceededA serializable read-write transaction exceeded the server lifetime cap.

Use SerializableRetryHelper.IsRetryable(...) when you own the retry loop:

catch (CamusException ex) when (SerializableRetryHelper.IsRetryable(ex))
{
// Replay the whole transaction from the beginning.
}

For single-statement autocommit work, use SerializableRetryHelper.ExecuteAutocommitAsync(...):

await SerializableRetryHelper.ExecuteAutocommitAsync(async ct =>
{
CamusTransaction tx = await connection.BeginTransactionAsync(ct);
try
{
await using CamusCommand update = connection.CreateCamusCommand("""
UPDATE robots SET price = @price WHERE name = @name
""");

update.Transaction = tx;
update.Parameters.Add("@price", ColumnType.Float64, 99.0);
update.Parameters.Add("@name", ColumnType.String, "T-800");

await update.ExecuteNonQueryAsync(ct);
await tx.CommitAsync(ct);
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
}, maxAttempts: 5, cancellationToken);

For explicit multi-statement transactions, do not retry only the failed statement. Start a new transaction and rerun every read and write in the unit:

const int MaxAttempts = 5;

for (int attempt = 1; ; attempt++)
{
CamusTransaction tx = await connection.BeginTransactionAsync();
try
{
long balance = await ReadBalance(tx, accountId);
if (balance < amount)
throw new InvalidOperationException("Insufficient funds");

await Debit(tx, accountId, balance - amount);
await tx.CommitAsync();
break;
}
catch (CamusException ex) when (SerializableRetryHelper.IsRetryable(ex))
{
await tx.RollbackAsync();
if (attempt >= MaxAttempts)
throw;

await Task.Delay(20 * (1 << attempt));
}
catch
{
await tx.RollbackAsync();
throw;
}
}

The helper's default backoff is bounded exponential delay with jitter: min(20 ms * 2^attempt, 400 ms) plus or minus 25 percent.

ADO.NET Notes

  • The provider uses HTTP under the hood.
  • Cancel() is cooperative through cancellation tokens.
  • Concurrent reads can share a connection session.
  • Transaction-scoped commands are pinned to the transaction endpoint, which is important when the connection string contains multiple endpoints.

When To Use It

Use the ADO.NET provider when you want:

  • full control over SQL text
  • direct use of CamusDB-specific SQL features
  • lightweight integration without EF Core
  • explicit transaction handling

For higher-level ORM usage, see EF Core Provider.