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+.
# 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
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.
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");
}
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();
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
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.
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 }
});
| Op | Notes |
|---|---|
Where | ==, !=, <, <=, >, >=, &&, ||, !, string.Contains/StartsWith/EndsWith, IEnumerable.Contains, dotted property paths |
OrderBy / OrderByDescending / ThenBy / ThenByDescending | — |
Skip / Take | Maps to server skip / limit |
Select | Client-side projection (any output type, including anonymous) |
First / FirstOrDefault | With or without predicate |
Single / SingleOrDefault | Pulls 2, throws on duplicates |
Count / Any | Index-friendly; Count hits the dedicated server endpoint |
Sum / Min / Max / Average | Lowered to a $group aggregation pipeline |
// 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();
Name → "Name").Id maps to _id on the wire.[JsonPropertyName("...")] — works in both query translation and serialization.Tier matches "tier" and "Tier".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");
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.
// 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);
// 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.
// $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 } });
await client.DeleteAsync("sessions", new { expired = true });
await client.DeleteOneAsync("users", new { _id = userId });
int total = await client.CountAsync("users");
int active = await client.CountAsync("users", new { active = true });
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 }
});
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;
}
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.
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");
// Reclaim space from soft-deleted records
await client.CompactAsync("users");
// Inspect collections
var names = await client.ListCollectionsAsync();
| Package | Process model | Use when |
|---|---|---|
OxiDb.Client.Tcp | Talks to a separate oxidb-server | Multiple apps share the DB, you want auth/RBAC, container deploys |
OxiDb.Client.Embedded | In-process via native FFI | Single-process apps, desktop tools, short-lived workers, tests |
OxiDb.Linq | Layered on either of the above | You want LINQ syntax (Where/OrderBy/...) over a document collection |