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 net8.0 and net9.0 and depends on EF Core 9 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")
.Options;

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.

Type Mapping

Supported CLR-to-store mappings:

CLR typeCamus store typeDDL type
string keyid or oidOID
Guid keyid or oidOID
stringstringSTRING
boolboolBOOL
short, int, longint64INT64
float, doublefloat64FLOAT64

Practical rule:

  • use HasColumnType("id") for CamusDB ObjectId primary keys
  • use HasColumnType("string"), HasColumnType("int64"), HasColumnType("float64"), and HasColumnType("bool") for regular columns

Create Tables

EnsureCreated() is supported:

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

The target CamusDB database must already exist. EnsureCreated() builds CREATE TABLE statements from the model and continues safely if a table already exists; it does not create or drop the database container itself.

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

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.

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 ...
Add columnALTER TABLE ... ADD COLUMN ...
Drop columnALTER TABLE ... DROP COLUMN ...
Create indexCREATE INDEX IF NOT EXISTS ...
Create unique indexCREATE UNIQUE INDEX IF NOT EXISTS ...
Drop indexALTER TABLE ... DROP INDEX ...
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);
}
}

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
  • no ALTER COLUMN
  • no rename table/column/index operations through EF migrations; use SQL DDL directly for ALTER TABLE ... RENAME ...
  • no check constraints
  • no sequences
  • no add/drop primary key through migrations
  • no inline unique constraints in migrations; use unique indexes instead
  • no drop-database support through the provider

Model restrictions:

  • key CLR types must be string, Guid, short, int, or long
  • [ConcurrencyCheck] is limited to numeric columns

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.