Skip to main content

EF Core Provider

CamusDB also ships an Entity Framework Core provider built on top of the ADO.NET driver. The package name is CamusDB.EntityFrameworkCore.

It targets net10.0 and depends on EF Core 10 relational APIs.

Install

dotnet add package CamusDB.EntityFrameworkCore

Configure The Provider

Register the provider with UseCamusDB(...):

using CamusDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

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

The connection string accepts the same keys as the ADO.NET driver, including Protocol=grpc, User, Password, AccessToken, MaxAutoPrepare, and AutoPrepareMinUsages. See .NET Driver for the full connection string reference and .NET Driver Authentication for authenticated connections.

You can also configure it in OnConfiguring:

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

Reuse An Existing Connection

If you want to share a CamusConnection or manage transactions outside the context, pass the connection directly:

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

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

When you pass an existing connection, the DbContext does not own it and will not dispose it.

Define A Model

Map primary-key object ids with store type "id" or "oid", and mark them as generated on add if you want client-side ObjectId generation:

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

public sealed 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();

b.Property(e => e.Name).HasColumnType("string");
b.Property(e => e.Kind).HasColumnType("string");
b.Property(e => e.Year).HasColumnType("int64");
b.Property(e => e.Price).HasColumnType("float64");
b.Property(e => e.Enabled).HasColumnType("bool");
});
}
}

For string primary keys mapped to "id" or "oid", the provider generates a 24-character Camus ObjectId on the client side.

Use ToTable(t => t.HasCheckConstraint(...)) for server-enforced check constraints:

modelBuilder.Entity<Robot>(b =>
{
b.ToTable("robots", t =>
t.HasCheckConstraint("ck_robots_price", "price >= 0"));
});

Rows rejected by a check constraint surface as DbUpdateException with an inner CamusException carrying CADB0303.

Type Mapping

Supported CLR-to-store mappings:

CLR typeCamus store typeDDL type
string keyid or oidOID
Guid keyid or oidOID
Guid with HasColumnType("uuid")uuid or guidUUID
stringstringSTRING
string with HasMaxLength(n)stringSTRING(n)
boolboolBOOL
short, int, longint64INT64
floatfloat32 or realFLOAT32
doublefloat64FLOAT64
byte[]bytes or blobBYTES
DateOnlydateDATE
DateTime, DateTimeOffsetdatetime or timestampDATETIME

Practical rule:

  • use HasColumnType("id") for CamusDB ObjectId primary keys
  • use HasColumnType("uuid") for native UUID columns
  • use HasColumnType("string"), HasColumnType("int64"), HasColumnType("float32"), HasColumnType("float64"), HasColumnType("bytes"), HasColumnType("date"), HasColumnType("datetime"), and HasColumnType("bool") for regular columns

A plain Guid property defaults to the id / OID mapping for backward compatibility. For UUID application values, explicitly map the property as uuid:

b.Property(e => e.ExternalRef).HasColumnType("uuid");

DateTime and DateTimeOffset values are normalized to UTC. DateOnly maps to calendar dates. The EF provider does not map ARRAY(T) columns; use the ADO.NET driver when you need array parameters.

Create Tables

EnsureCreated() is supported and idempotent:

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

EnsureCreated() creates the database with IF NOT EXISTS and then creates the model tables. It continues safely if the database or tables already exist.

EnsureDeleted() drops the database named in the connection string:

await ctx.Database.EnsureDeletedAsync();

Basic CRUD

Insert

await using var ctx = new AppDbContext(options);

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

await ctx.SaveChangesAsync();

Query

await using var ctx = new AppDbContext(options);

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

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

Update

await using var ctx = new AppDbContext(options);

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(options);

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

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

Transactions

The provider supports EF Core transactions:

await using var ctx = new AppDbContext(options);
await using var tx = await ctx.Database.BeginTransactionAsync();

ctx.Robots.Add(new Robot { Name = "R2-D2", Kind = "mechanical", Year = 1977 });
await ctx.SaveChangesAsync();

await tx.CommitAsync();

With pooled endpoints, transaction-scoped commands stay pinned to the same node for the lifetime of the transaction.

Retry On Serializable Conflicts

Serializable is CamusDB's default isolation level. SaveChangesAsync() or CommitTransactionAsync() can fail when a concurrent transaction wins a serialization conflict.

Enable EF Core's execution strategy with EnableRetryOnFailure():

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

Only the retryable CamusDB transaction errors are retried:

  • CADB0502 TransactionConflict
  • CADB0504 TransactionMustRetry
  • CADB0505 TransactionLifetimeExceeded

CADB0509 TransactionFinalizeUnresolved is not a replay-from-beginning signal. If it surfaces while finalizing a transaction, retry the same commit or rollback for the same transaction id instead of rerunning the unit of work.

Default retry settings:

ParameterDefaultDescription
maxRetryCount15Maximum retry attempts.
maxRetryDelay1 sMaximum delay between retries.
retryDeadline5 sWall-clock deadline from first failure.
medianFirstRetryDelay30 msMedian first retry delay.

You can override them:

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

The execution strategy retries the entire EF operation. If you manage explicit transactions manually, replay the whole transaction from the beginning instead of retrying only the failed statement.

Prepared Statements

EF Core queries and SaveChangesAsync() statements are prepared automatically by the underlying ADO.NET driver once the same SQL shape has executed enough times. No EF-specific configuration or manual Prepare() call is required.

This works well with EF because LINQ queries and change-tracking writes usually produce deterministic parameterized SQL for a given operation. Prepared execution preserves the same transaction, Serializable isolation, retry, and query-result-cache behavior as inline execution.

Tune the policy with the normal connection-string keys:

var options = new DbContextOptionsBuilder<AppDbContext>()
.UseCamusDB(
"Endpoint=http://localhost:5095;Database=mydb;MaxAutoPrepare=256;AutoPrepareMinUsages=2")
.Options;

Set MaxAutoPrepare=0 to disable automatic preparation.

See .NET Driver and Prepared Statements for the full behavior.

Query Result Cache

Use WithCache(...) to opt a LINQ query into CamusDB's query result cache. The provider injects the {cache=...} hint into generated SQL.

using CamusDB.EntityFrameworkCore;

List<Order> recent = await ctx.Orders
.Where(o => o.Status == 1)
.OrderByDescending(o => o.Total)
.Take(20)
.WithCache("recent_orders", ttl: TimeSpan.FromSeconds(30), strict: true)
.ToListAsync();

The cache only serves single-table, autocommit reads. Queries with joins or queries inside explicit transactions read live storage. Evict cache entries through the underlying CamusConnection with EvictCacheAsync(...) or EvictAllCacheAsync().

Migrations

The provider includes design-time services, so standard EF tooling can discover it:

dotnet ef migrations add InitialCreate
dotnet ef database update

Supported migration operations:

OperationSQL shape
Create tableCREATE TABLE IF NOT EXISTS ...
Drop tableDROP TABLE ...
Rename tableALTER TABLE ... RENAME TO ...
Add columnALTER TABLE ... ADD COLUMN ...
Drop columnALTER TABLE ... DROP COLUMN ...
Rename columnALTER TABLE ... RENAME COLUMN ... TO ...
Alter column nullabilityALTER TABLE ... ALTER COLUMN ... SET NOT NULL or DROP NOT NULL
Add check constraintALTER TABLE ... ADD CONSTRAINT ... CHECK (...)
Drop check constraintALTER TABLE ... DROP CONSTRAINT ...
Create indexCREATE INDEX IF NOT EXISTS ...
Create unique indexCREATE UNIQUE INDEX IF NOT EXISTS ...
Drop indexALTER TABLE ... DROP INDEX ...
Rename indexALTER TABLE ... RENAME INDEX ... TO ...
Seed dataINSERT INTO ... VALUES (...)
Raw SQLpassed through as-is

Example:

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);

migrationBuilder.AddCheckConstraint(
name: "ck_products_stock",
table: "products",
sql: "Stock >= 0");
}
}

Changing a column's nullability is supported when the stored type does not change. Changing a column's type is not supported in place.

DropCheckConstraint emits DROP CONSTRAINT; CamusDB resolves that against both named CHECK constraints and named NOT NULL constraints.

Concurrency

[ConcurrencyCheck] is supported on numeric properties:

  • short
  • int
  • long

Example:

public sealed class Order
{
public string Id { get; set; } = "";
public string Status { get; set; } = "";

[ConcurrencyCheck]
public long Version { get; set; }
}

You must increment the version column in application code before saving:

order.Status = "shipped";
order.Version++;
await ctx.SaveChangesAsync();

Important behavior:

  • CamusDB detects write conflicts at transaction commit time.
  • SaveChangesAsync() can succeed and the conflict can still surface later at CommitTransactionAsync().
  • For optimistic concurrency, use an explicit numeric version column with [ConcurrencyCheck].

[Timestamp] is not supported.

Current Limitations

The provider is useful today, but it does not try to emulate unsupported database features.

Unsupported or restricted operations include:

  • no foreign key constraints
  • no computed columns
  • ALTER COLUMN only supports toggling nullability; changing the stored type requires a manual migration strategy
  • no sequences
  • no add/drop primary key through migrations
  • no inline unique constraints in migrations; use unique indexes instead
  • no ARRAY(T) property mapping; use the ADO.NET driver for array parameters

Model restrictions:

  • key CLR types must be string, Guid, short, int, or long
  • [ConcurrencyCheck] is limited to numeric columns
  • plain Guid maps to OID; use HasColumnType("uuid") for native UUID storage
  • WithCache(...) is effective only on single-table autocommit reads

When To Use It

Use the EF provider when you want:

  • LINQ over CamusDB tables
  • EF Core change tracking
  • EnsureCreated() or EF migrations for supported DDL
  • application-level optimistic concurrency with version columns

For direct SQL-first access or finer control over commands and transactions, see .NET Driver.