CamusDB.Client 0.4.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package CamusDB.Client --version 0.4.2
                    
NuGet\Install-Package CamusDB.Client -Version 0.4.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CamusDB.Client" Version="0.4.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CamusDB.Client" Version="0.4.2" />
                    
Directory.Packages.props
<PackageReference Include="CamusDB.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CamusDB.Client --version 0.4.2
                    
#r "nuget: CamusDB.Client, 0.4.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CamusDB.Client@0.4.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CamusDB.Client&version=0.4.2
                    
Install as a Cake Addin
#tool nuget:?package=CamusDB.Client&version=0.4.2
                    
Install as a Cake Tool

CamusDB Connector for .NET

.NET idiomatic client libraries for CamusDB

This repository contains two packages:

Package Description
CamusDB.Client ADO.NET provider — recommended for direct database access from .NET
CamusDB.EntityFrameworkCore Entity Framework Core provider built on top of CamusDB.Client

CamusDB.Client (ADO.NET)

Installation

dotnet add package CamusDB.Client

Or via the Package Manager Console:

Install-Package CamusDB.Client

Configuration

Create a CamusConnectionStringBuilder with a connection string:

using CamusDB.Client;

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

await connection.OpenAsync();

Supported connection string keys:

Key Required Description
Endpoint Yes Base URL for the CamusDB HTTP endpoint.
Database Yes Database name sent with requests.

Endpoint also supports a comma-separated pool. The client selects endpoints with round-robin routing:

CamusConnectionStringBuilder builder = new(
    "Endpoint=http://localhost:8082,http://localhost:8084,http://localhost:8086;Database=test");

When a request fails because an endpoint is unreachable, that endpoint is marked unavailable and skipped by later requests made through the same CamusConnectionStringBuilder.

Usage

Ping
await using CamusCommand ping = connection.CreatePingCommand();

int result = await ping.ExecuteNonQueryAsync();
Execute DDL
await using CamusCommand command = connection.CreateCamusCommand("""
    CREATE TABLE robots (
        id OID PRIMARY KEY NOT NULL,
        name STRING NOT NULL,
        type STRING,
        year INT64,
        price FLOAT64,
        enabled BOOL
    )
    """);

bool created = await command.ExecuteDDLAsync();
Insert Rows
using CamusDB.Core.Util.ObjectIds;

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

insert.Parameters.Add("id", ColumnType.Id, CamusObjectIdGenerator.Generate());
insert.Parameters.Add("name", ColumnType.String, "T-800");
insert.Parameters.Add("type", 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 insertedRows = await insert.ExecuteNonQueryAsync();

You can also execute parameterized SQL:

const string sql = """
    INSERT INTO robots (id, name, year, type, price, enabled)
    VALUES (GEN_ID(), @name, @year, @type, @price, @enabled)
    """;

await using CamusCommand insert = connection.CreateCamusCommand(sql);

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

int insertedRows = await insert.ExecuteNonQueryAsync();
Select Rows
await using CamusCommand select = connection.CreateSelectCommand(
    "SELECT * FROM robots WHERE year = @year");

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

CamusDataReader reader = await select.ExecuteReaderAsync();

while (await reader.ReadAsync())
{
    string id   = reader.GetString(0);
    string name = reader.GetString(1);
    string type = reader.GetString(2);
    long   year = reader.GetInt64(3);
}
Transactions
CamusTransaction transaction = await connection.BeginTransactionAsync();

await using CamusCommand insert = connection.CreateInsertCommand("robots");
insert.Transaction = transaction;

insert.Parameters.Add("id", ColumnType.Id, CamusObjectIdGenerator.Generate());
insert.Parameters.Add("name", ColumnType.String, "HAL 9000");
insert.Parameters.Add("type", ColumnType.String, "electronic");
insert.Parameters.Add("year", ColumnType.Integer64, 1968);
insert.Parameters.Add("price", ColumnType.Float64, 42.0);
insert.Parameters.Add("enabled", ColumnType.Bool, true);

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

Use await transaction.RollbackAsync() to roll back instead.

Serializable Isolation & Retries

Serializable is the default isolation level in CamusDB. When two serializable transactions conflict, one is aborted immediately and must be replayed from BEGIN — retrying a single statement is not safe.

Three error codes indicate a transient conflict that a full retry can resolve:

Code Name When raised
CADB0502 TransactionConflict Lock conflict; server aborted at lock-acquire time
CADB0504 TransactionMustRetry Routing retry budget exhausted; no data written
CADB0505 TransactionLifetimeExceeded Transaction held open past the server lifetime limit

Use SerializableRetryHelper.IsRetryable(ex) to test any exception, and SerializableRetryHelper.ExecuteAutocommitAsync for bounded automatic retry of single-statement (autocommit) operations:

await SerializableRetryHelper.ExecuteAutocommitAsync(async ct =>
{
    CamusTransaction tx = await connection.BeginTransactionAsync(ct);
    try
    {
        await using CamusCommand cmd = connection.CreateCamusCommand(
            "UPDATE robots SET price = @price WHERE name = @name");
        cmd.Transaction = tx;
        cmd.Parameters.Add("@price", ColumnType.Float64, 99.0);
        cmd.Parameters.Add("@name",  ColumnType.String,  "T-800");
        await cmd.ExecuteNonQueryAsync(ct);
        await tx.CommitAsync(ct);
    }
    catch
    {
        await tx.RollbackAsync(ct);
        throw;
    }
}, maxAttempts: 5, cancellationToken);

Back-off schedule: min(20 ms × 2^attempt, 400 ms) ± 25 % jitter. Any non-retryable exception propagates immediately.

For explicit multi-statement transactions, own the retry loop yourself so you can replay every read and write from scratch:

const int MaxAttempts = 5;
int attempt = 0;

while (true)
{
    CamusTransaction tx = await connection.BeginTransactionAsync();
    try
    {
        // re-execute ALL reads and writes on every attempt
        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;
    }
}

CamusDB.EntityFrameworkCore (EF Core)

Installation

dotnet add package CamusDB.EntityFrameworkCore

Or via the Package Manager Console:

Install-Package CamusDB.EntityFrameworkCore

Configuration

Register the provider via UseCamusDB in your DbContext options:

using CamusDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseCamusDB("Endpoint=http://localhost:8082;Database=mydb")
    .Options;

Or configure it inside OnConfiguring:

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseCamusDB("Endpoint=http://localhost:8082;Database=mydb");
}
Retry on failure

Call EnableRetryOnFailure on the CamusDBDbContextOptionsBuilder to let EF Core automatically retry SaveChangesAsync (and query execution) when a transient serialization conflict is detected. Only the three retryable CamusDB error codes (CADB0502, CADB0504, CADB0505) trigger a retry — all other exceptions propagate immediately.

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseCamusDB("Endpoint=http://localhost:8082;Database=mydb", o =>
    {
        o.EnableRetryOnFailure();
    })
    .Options;

Default parameters:

Parameter Default Description
maxRetryCount 15 Maximum number of retry attempts
maxRetryDelay 1 s Upper bound on the delay between retries
retryDeadline 5 s Wall-clock deadline from first failure; no further retries after this
medianFirstRetryDelay 30 ms Median delay before the first retry

Override any parameter explicitly:

o.EnableRetryOnFailure(
    maxRetryCount: 5,
    maxRetryDelay: TimeSpan.FromMilliseconds(500),
    retryDeadline: TimeSpan.FromSeconds(3),
    medianFirstRetryDelay: TimeSpan.FromMilliseconds(20));

EF Core's execution strategy retries the entire unit of work — never only the failing statement. If you manage transactions manually with BeginTransactionAsync / CommitTransactionAsync, use SerializableRetryHelper in CamusDB.Client instead.

Sharing an existing connection

Pass an open CamusConnection when you want to share a connection or attach to an externally managed transaction:

CamusConnection connection = await GetOpenConnectionAsync();

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseCamusDB(connection)
    .Options;

await using var ctx = new AppDbContext(options);
await ctx.Database.BeginTransactionAsync();
// ... SaveChangesAsync, then CommitTransactionAsync

The DbContext does not take ownership of the supplied connection and will not close or dispose it.

Defining a Model

Use standard EF Core data annotations or the fluent API. Map ID columns to the "id" store type and call ValueGeneratedOnAdd() so the provider generates a client-side ObjectId automatically:

public class Robot
{
    public string Id   { get; set; } = "";
    public string Name { get; set; } = "";
    public string Type { get; set; } = "";
    public int    Year { get; set; }
    public double Price   { get; set; }
    public bool   Enabled { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<Robot> Robots => Set<Robot>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Robot>(b =>
        {
            b.ToTable("robots");
            b.HasKey(e => e.Id);
            b.Property(e => e.Id)
             .HasColumnType("id")
             .ValueGeneratedOnAdd();   // client-side ObjectId generation
            b.Property(e => e.Name).HasColumnType("string");
            b.Property(e => e.Type).HasColumnType("string");
            b.Property(e => e.Year).HasColumnType("int64");
            b.Property(e => e.Price).HasColumnType("float64");
            b.Property(e => e.Enabled).HasColumnType("bool");
        });
    }
}

CamusDB Type Mapping

CLR type CamusDB store type DDL type
string (ID / PK) id or oid OID
Guid (ID / PK) id or oid OID
string string STRING
bool bool BOOL
short, int, long int64 INT64
float, double float64 FLOAT64

Use HasColumnType("id") (or the alias "oid") for primary key columns backed by CamusDB ObjectIds. The provider sends the value as an OID on the wire regardless of whether the CLR property is string or Guid.

Creating Tables

EnsureCreated() creates all tables defined in the model. It is safe to call on a database that already has the tables:

await using var ctx = new AppDbContext();
await ctx.Database.EnsureCreatedAsync();

Insert

await using var ctx = new AppDbContext();

ctx.Robots.Add(new Robot
{
    Name    = "T-800",
    Type    = "cyborg",
    Year    = 1984,
    Price   = 10.0,
    Enabled = true
});

await ctx.SaveChangesAsync(); // Id is generated automatically

Query

await using var ctx = new AppDbContext();

// Key lookup
Robot? robot = await ctx.Robots.FindAsync(id);

// LINQ predicate
List<Robot> active = await ctx.Robots
    .Where(r => r.Enabled && r.Year > 1980)
    .ToListAsync();

Update

await using var ctx = new AppDbContext();

Robot robot = await ctx.Robots.FindAsync(id)
    ?? throw new InvalidOperationException("Not found");

robot.Price = 99.0;
await ctx.SaveChangesAsync();

Delete

await using var ctx = new AppDbContext();

Robot robot = await ctx.Robots.FindAsync(id)
    ?? throw new InvalidOperationException("Not found");

ctx.Robots.Remove(robot);
await ctx.SaveChangesAsync();

Migrations

The provider supports EF Core migrations for the following DDL operations:

Operation Generated SQL
Create table CREATE TABLE t (col TYPE [PRIMARY KEY NOT NULL \| NOT NULL], ...)
Drop table DROP TABLE t
Add column ALTER TABLE t ADD COLUMN col TYPE [NOT NULL] [DEFAULT (value)]
Drop column ALTER TABLE t DROP COLUMN col
Create index CREATE INDEX name ON t (col1, col2)
Create unique index CREATE UNIQUE INDEX name ON t (col1, col2)
Drop index ALTER TABLE t DROP INDEX name
Raw SQL passed through as-is

The provider ships design-time services so the EF tooling can discover the provider automatically. No extra flags are needed:

dotnet ef migrations add InitialCreate
dotnet ef database update

Example migration using the supported operations:

public partial class AddStockColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<int>(
            name: "Stock",
            table: "products",
            type: "int64",
            nullable: false,
            defaultValue: 0);

        migrationBuilder.CreateIndex(
            name: "idx_products_name",
            table: "products",
            column: "Name",
            unique: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropIndex(name: "idx_products_name", table: "products");
        migrationBuilder.DropColumn(name: "Stock", table: "products");
    }
}

Concurrency Tokens

[ConcurrencyCheck] is supported on numeric columns (short, int, long). The application is responsible for incrementing the version column before calling SaveChanges() — CamusDB has no server-side auto-increment version type:

public class Order
{
    public string Id      { get; set; } = "";
    public string Status  { get; set; } = "";
    [ConcurrencyCheck]
    public long Version   { get; set; }
}

// On update: increment Version manually so EF adds AND Version = @original_version to the WHERE
order.Status = "shipped";
order.Version++;
await ctx.SaveChangesAsync();

[Timestamp] (byte array row version) is not supported.

Note on MVCC concurrency: CamusDB uses multi-version concurrency control (MVCC). Write-write conflicts between transactions are detected at commit time, not during the write phase. This means SaveChangesAsync will succeed even when two open transactions have both updated the same row — the conflict surfaces when the second transaction calls CommitTransactionAsync. Application-level optimistic concurrency via [ConcurrencyCheck] (above) is the recommended pattern for detecting stale updates.

Provider Limitations

  • No computed columns.
  • No foreign key constraints.
  • No ALTER COLUMN — changing a column type requires dropping and recreating the column.
  • No RENAME COLUMN, RENAME TABLE, or RENAME INDEX.
  • Key CLR types must be one of: string, int, long, short, or Guid.
  • [ConcurrencyCheck] is only supported on short, int, and long columns; [Timestamp] is not supported.
  • MVCC conflict detection occurs at commit time, not during SaveChangesAsync. Use application-level version columns with [ConcurrencyCheck] for optimistic concurrency.

Run Tests

To run the unit tests, a CamusDB instance must be running locally. After starting it, run:

dotnet test -l "console;verbosity=normal" --filter "FullyQualifiedName~CamusDB.Client.Tests"

Contribution

CamusDB.Client is an open-source project, and contributions are heartily welcomed! Whether you are looking to fix bugs, add new features, or improve documentation, your efforts and contributions will be appreciated. Check out the CONTRIBUTING.md file for guidelines on how to get started with contributing to CamusDB.Client.

License

CamusDB.Client is released under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CamusDB.Client:

Package Downloads
CamusDB.EntityFrameworkCore

CamusDB.EntityFrameworkCore provides Entity Framework Core support for CamusDB.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 53 6/25/2026
0.4.6 114 6/21/2026
0.4.5 123 6/18/2026
0.4.2 111 6/15/2026
0.4.1 118 6/15/2026
0.4.0 97 6/15/2026
0.3.9 118 6/15/2026
0.3.7 122 6/7/2026
0.3.6 113 6/7/2026
0.3.5 123 6/7/2026
0.3.4 127 6/7/2026
0.3.3 126 6/7/2026
0.3.1 114 6/7/2026
0.3.0 120 6/7/2026
0.2.2-alpha 106 6/4/2026
0.2.1-alpha 107 6/1/2026
0.2.0-alpha 96 5/29/2026
0.1.1-alpha 9,719 7/31/2024
0.0.9-alpha 17,515 2/7/2024
0.0.8-alpha 188 2/5/2024
Loading failed