.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 endpointDatabase: 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:
| Key | Required | Description |
|---|---|---|
Endpoint | Yes | Base URL for the CamusDB node. |
Database | Yes | Database 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:
GetStringGetBooleanGetInt16/GetInt32/GetInt64GetFloat/GetDoubleGetGuidIsDBNull
Parameters
Parameters are input-only. Supported value mappings include:
| Camus type | Typical .NET values |
|---|---|
ColumnType.Id | string, Guid, Camus object id values |
ColumnType.String | string |
ColumnType.Integer64 | short, int, long, other integer-convertible values |
ColumnType.Float64 | float, double |
ColumnType.Bool | bool |
ColumnType.Null | null, 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:
| Code | Name | Meaning |
|---|---|---|
CADB0502 | TransactionConflict | A lock conflict aborted the transaction. |
CADB0504 | TransactionMustRetry | A transient routing or leader-transition condition exhausted the commit retry budget. |
CADB0505 | TransactionLifetimeExceeded | A 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.