.NET Examples

A full .NET stack for OxiDB — a TCP client, an embedded (in-process FFI) client, a standalone OxiDb.Linq provider for the document engine, and a complete Entity Framework Core provider (with ADO.NET / Dapper) for the SQL engine. Targets net10.0+.

Install

# Document engine — TCP client, embedded client, LINQ provider
dotnet add package OxiDb.Client.Tcp
dotnet add package OxiDb.Client.Embedded
dotnet add package OxiDb.Linq

# SQL engine — Entity Framework Core provider + ADO.NET (Dapper-ready)
dotnet add package OxiDb.EntityFrameworkCore
dotnet add package OxiDb.Data

Latest: v0.42.0. The EF Core packages are hosted on this site — add it as a NuGet source, or download the .nupkg files directly:

# download the EF Core provider + its dependencies
curl -LO https://oxidb.baltavista.com/nuget/OxiDb.EntityFrameworkCore.0.42.0.nupkg
curl -LO https://oxidb.baltavista.com/nuget/OxiDb.Data.0.42.0.nupkg
curl -LO https://oxidb.baltavista.com/nuget/OxiDb.Client.Tcp.0.42.0.nupkg

# add the folder as a source, then install
dotnet nuget add source $(pwd) --name oxidb
dotnet add package OxiDb.EntityFrameworkCore

Entity Framework Core SQL engine

A complete EF Core provider for the OxiDB SQL engine — it passes all 3832 official EF Core relational specification tests and beats PostgreSQL across the EF Core benchmark. Migrations, design-time scaffolding, LINQ translation, Include, and ExecuteUpdate/ExecuteDelete all work. Start the server with OXIDB_SQL=1.

DbContext

using Microsoft.EntityFrameworkCore;
using OxiDb.EntityFrameworkCore;

public class Blog {
    public int Id { get; set; }
    public string Url { get; set; } = "";
    public int Rating { get; set; }
    public List<Post> Posts { get; } = new();
}
public class Post {
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public int BlogId { get; set; }
}

public class BloggingContext : DbContext {
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();

    protected override void OnConfiguring(DbContextOptionsBuilder o) =>
        o.UseOxiDb("Host=127.0.0.1;Port=4444");
}

Migrate & query

using var db = new BloggingContext();
db.Database.Migrate();                         // real __EFMigrationsHistory

db.Blogs.Add(new Blog { Url = "https://oxidb.dev", Rating = 5 });
await db.SaveChangesAsync();

// LINQ with Include + ordering (translated to SQL joins)
var top = await db.Blogs
    .Where(b => b.Rating >= 4)
    .Include(b => b.Posts)
    .OrderByDescending(b => b.Rating)
    .ToListAsync();

// Bulk update / delete — one UPDATE / DELETE round trip
await db.Posts.Where(p => p.BlogId == 1)
    .ExecuteUpdateAsync(s => s.SetProperty(p => p.Title, "Updated"));
await db.Blogs.Where(b => b.Rating < 2).ExecuteDeleteAsync();

Migrations & scaffolding (dotnet ef)

dotnet ef migrations add Init
dotnet ef database update

# reverse-engineer a DbContext from an existing database
dotnet ef dbcontext scaffold "Host=127.0.0.1;Port=4444" OxiDb.EntityFrameworkCore

Raw SQL — ADO.NET & Dapper

using OxiDb.Data;
using Dapper;

using var conn = new OxiDbConnection("Host=127.0.0.1;Port=4444");
conn.Open();

// Dapper
var blogs = conn.Query<Blog>("SELECT * FROM Blogs WHERE Rating >= @r", new { r = 4 });

// or plain ADO.NET
using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO Blogs (Url, Rating) VALUES (?, ?)";
cmd.Parameters.Add(new OxiDbParameter { Value = "https://x.io" });
cmd.Parameters.Add(new OxiDbParameter { Value = 4 });
cmd.ExecuteNonQuery();

Full SQL surface — joins, CTEs, window functions, transactions, instant online ALTER TABLE — on the SQL Engine page.

OxiDb.Linq — standalone LINQ provider net10.0

Lightweight, EF-free LINQ over OxiDB. IQueryable<T> with Where / OrderBy / Skip / Take / First / Count / Any / Sum / projection, plus fluent SetAsync / IncAsync / DeleteAsync after a filter. Think MongoDB.Driver's IMongoQueryable, but for OxiDB.

using OxiDb.Client.Tcp;
using OxiDb.Linq;

await using var client = await OxiDbTcpClient.ConnectAsync("127.0.0.1", 4444);
var users = client.GetCollection<User>("users");

// Insert
await users.InsertAsync(new User { Name = "Alice", Age = 30, Country = "TR" });

// Where + OrderBy + Take + materialise
var top = await users
    .Where(u => u.Country == "TR" && u.Age >= 18)
    .OrderByDescending(u => u.Age)
    .Take(10)
    .ToListAsync();

// FirstOrDefault with predicate
var alice = await users.FirstOrDefaultAsync(u => u.Email == "alice@x.com");

// Aggregations
int n     = await users.CountAsync(u => u.Active);
decimal total = await users.Where(u => u.Active).SumAsync(u => u.Spend);
bool anyVip  = await users.AnyAsync(u => u.Tier == "vip");

// Projection
var names = await users
    .Where(u => u.Active)
    .Select(u => new { u.Name, u.Email })
    .ToListAsync();

// String predicates → $regex
var gmail = await users
    .Where(u => u.Email.EndsWith("@gmail.com"))
    .ToListAsync();

// Fluent updates / deletes after a Where
await users.Where(u => u.Country == "TR").SetAsync(new { tier = "gold" });
await users.Where(u => u.Email == "alice@x.com").IncAsync(new { loginCount = 1 });
await users.Where(u => u.Expired).DeleteAsync();

// Raw escape hatch — full Mongo-style update document
await users.Where(u => u._id == id).UpdateAsync(new Dictionary<string, object?> {
    ["$set"] = new { lastLogin = DateTime.UtcNow },
    ["$inc"] = new { loginCount = 1 }
});

Supported LINQ surface

OpNotes
Where==, !=, <, <=, >, >=, &&, ||, !, string.Contains/StartsWith/EndsWith, IEnumerable.Contains, dotted property paths
OrderBy / OrderByDescending / ThenBy / ThenByDescending
Skip / TakeMaps to server skip / limit
SelectClient-side projection (any output type, including anonymous)
First / FirstOrDefaultWith or without predicate
Single / SingleOrDefaultPulls 2, throws on duplicates
Count / AnyIndex-friendly; Count hits the dedicated server endpoint
Sum / Min / Max / AverageLowered to a $group aggregation pipeline

Mutation extensions

// Build the filter with LINQ, then mutate.
await q.SetAsync(new { ... });        // $set
await q.UnsetAsync(new { ... });      // $unset
await q.IncAsync(new { ... });        // $inc
await q.PushAsync(new { ... });       // $push
await q.PullAsync(new { ... });       // $pull
await q.AddToSetAsync(new { ... });   // $addToSet
await q.UpdateAsync(new Dictionary<string, object?>{ ... });  // raw
await q.DeleteAsync();
await q.DeleteOneAsync();

Property-name mapping

  • By default, C# property names go to JSON 1:1 (Name"Name").
  • The single special case: a property called Id maps to _id on the wire.
  • Override with [JsonPropertyName("...")] — works in both query translation and serialization.
  • Deserialization is case-insensitive, so Tier matches "tier" and "Tier".

Connect — TCP client

using OxiDb.Client.Tcp;

// Default 127.0.0.1:4444
await using var client = new OxiDbTcpClient("127.0.0.1", 4444);
await client.PingAsync();

// With SCRAM-SHA-256 auth
await using var auth = new OxiDbTcpClient(
    host: "db.internal",
    port: 4444,
    username: "admin",
    password: "s3cret");

Connect — Embedded (no server)

using OxiDb.Client.Embedded;

// In-process. Native lib resolved automatically per platform.
using var db = new OxiDbEmbeddedClient("./data");
await db.PingAsync();

// Same IOxiDbClient surface as the TCP client — swap one constructor for the other.

Insert

// Single document — pass any object that serializes to JSON
await client.InsertAsync("users", new {
    name = "Alice",
    age = 30,
    department = "Engineering",
    tags = new[] { "vip", "early-access" }
});

// Bulk insert
var batch = Enumerable.Range(1, 1000).Select(i => new {
    name = $"User {i}", age = 20 + (i % 50)
});
await client.InsertManyAsync("users", batch);

Find & query

// Simple equality
var tokyo = await client.FindAsync("users",
    query: new { city = "Tokyo" });

// Comparison + sort + paging
var page = await client.FindAsync(
    "users",
    query: new { age = new { _gte = 25, _lt = 40 } },
    sort:  new { age = -1 },
    skip:  0,
    limit: 20);

// Logical operators
var admins = await client.FindAsync("users", new {
    _or = new[] {
        new { role = "admin" },
        new { role = "owner" }
    }
});

// Find one
var alice = await client.FindOneAsync("users", new { name = "Alice" });

The wire uses $gte, $lt, $or, etc. In C# anonymous types you can't start a name with $, so the SDK accepts _gte / _lt / _or and rewrites them on the wire.

Update

// $set, $inc, $push — combine in one call (OCC requirement)
await client.UpdateAsync(
    "users",
    query:  new { name = "Alice" },
    update: new {
        _set  = new { last_login = DateTime.UtcNow },
        _inc  = new { login_count = 1 },
        _push = new { recent_ips = "1.2.3.4" }
    });

// First match only
await client.UpdateOneAsync("sessions",
    new { user_id = id, expired = false },
    new { _set = new { expired = true } });

Delete

await client.DeleteAsync("sessions", new { expired = true });
await client.DeleteOneAsync("users", new { _id = userId });

Count

int total  = await client.CountAsync("users");
int active = await client.CountAsync("users", new { active = true });

Aggregation pipeline

var revenue = await client.AggregateAsync("orders", new[] {
    new { _match = new { status = "paid", year = 2026 } },
    new { _group = new {
        _id = "$category",
        total = new { _sum = "$amount" },
        n     = new { _sum = 1 }
    }},
    new { _sort  = new { total = -1 } },
    new { _limit = 10 }
});

Transactions

var tx = await client.BeginTransactionAsync();
try
{
    await tx.UpdateAsync("accounts", new { _id = fromId },
        new { _inc = new { balance = -amount } });
    await tx.UpdateAsync("accounts", new { _id = toId },
        new { _inc = new { balance =  amount } });
    await tx.InsertAsync("ledger", new { from = fromId, to = toId, amount });
    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}

Stored procedures (OxiScript)

await client.CreateProcedureAsync("transfer", @"
    proc transfer(from, to, amount) {
        let s = find_one(""accounts"", {_id: from})
        if s == null            { abort ""sender not found"" }
        if s.balance < amount   { abort ""insufficient funds"" }
        update(""accounts"", {_id: from}, {$inc: {balance: -amount}})
        update(""accounts"", {_id: to},   {$inc: {balance:  amount}})
        return {ok: true}
    }");

var result = await client.CallProcedureAsync("transfer",
    new { from = "alice", to = "bob", amount = 1500 });

Full OxiScript reference.

Indexes

await client.CreateIndexAsync("users", "email", unique: true);
await client.CreateIndexAsync("orders", "created_at");

var indexes = await client.ListIndexesAsync("users");
await client.DropIndexAsync("users", "old_field");

Maintenance

// Reclaim space from soft-deleted records
await client.CompactAsync("users");

// Inspect collections
var names = await client.ListCollectionsAsync();

TCP vs. Embedded — when to pick which

PackageProcess modelUse when
OxiDb.Client.TcpTalks to a separate oxidb-serverMultiple apps share the DB, you want auth/RBAC, container deploys
OxiDb.Client.EmbeddedIn-process via native FFISingle-process apps, desktop tools, short-lived workers, tests
OxiDb.LinqLayered on either of the aboveYou want LINQ syntax (Where/OrderBy/...) over a document collection
Report Issue