Official C# binding for the Ladybug embedded graph database. It wraps the native Ladybug C API via P/Invoke and ships prebuilt native libraries for supported platforms, so you can run Cypher queries against an embedded graph database directly from .NET.
Current package family:
0.17.1.0, built against the Ladybugv0.17.1engine.
net10.0(primary, AOT/trim friendly, source-generatedLibraryImport)netstandard2.0(broad reach, including .NET Framework)
| Package | Purpose | Extra dependencies |
|---|---|---|
LadybugDB |
Managed binding: Database/Connection/QueryResult/Value/PreparedStatement, async, engine-extension loading, the POCO source generator, and lightweight OpenTelemetry. |
none |
LadybugDB.Native (+ LadybugDB.Native.<rid>) |
Prebuilt native engine, one package per RID plus an all-platform meta-package. | — |
LadybugDB.Extensions |
ASP.NET/host integration: DI registration, health check, resilience (timeout/retry/circuit-breaker), IAsyncEnumerable streaming, typed row access, and result export (JSON/CSV/DataTable). |
Microsoft.Extensions.* |
LadybugDB.Arrow |
Apache Arrow interop: export a QueryResult to RecordBatches and ingest RecordBatches as tables. |
Apache.Arrow |
LadybugDB.Extensions and LadybugDB.Arrow are optional and isolated, so the core package and its
consumers stay dependency-free and AOT-friendly.
Reference the managed package plus a native package. For an app that should run on any supported platform, add the native meta-package (it pulls in every per-platform native package):
dotnet add package LadybugDB
dotnet add package LadybugDB.NativeFor a slim, single-platform app, reference just the native package for that platform instead:
dotnet add package LadybugDB
dotnet add package LadybugDB.Native.win-x64Available native packages: LadybugDB.Native.win-x64, LadybugDB.Native.linux-x64,
LadybugDB.Native.linux-arm64, LadybugDB.Native.osx-x64, LadybugDB.Native.osx-arm64. The
LadybugDB.Native meta-package depends on all of them.
using LadybugDB;
using var db = new Database("./demo.db"); // empty path => in-memory
using var conn = new Connection(db);
conn.Query("CREATE NODE TABLE Person(name STRING, age INT64, PRIMARY KEY(name))").Dispose();
conn.Query("CREATE (:Person {name: 'Alice', age: 30})").Dispose();
using var result = conn.Query("MATCH (p:Person) RETURN p.name, p.age");
foreach (var row in result.Rows())
{
Console.WriteLine($"{row[0]} is {row[1]} years old");
}Runnable samples live in examples/. Below is a tour of the capabilities beyond the basics.
conn.SetQueryTimeout(TimeSpan.FromSeconds(5)); // abort long queries
conn.SetMaxThreadsForExec(4);
// conn.Interrupt(); // cancel the in-flight query from another thread
using var r = conn.Query("MATCH (p:Person) RETURN p.name AS name, p.age AS age");
foreach (var col in r.Columns)
Console.WriteLine($"{col.Name}: {col.Type}"); // typed logical types
Console.WriteLine($"compiled in {r.Summary.CompilingTimeMs} ms, ran in {r.Summary.ExecutionTimeMs} ms");
// Multi-statement queries return every result set (no truncation):
foreach (var rs in conn.QueryAll("RETURN 1; RETURN 2;"))
using (rs) { /* ... */ }QueryResult also exposes ResetIterator(), HasNextQueryResult() / GetNextQueryResult().
LadybugDB offers a first-class async surface (the synchronous engine call is offloaded, honoring the
connection's internal serialization), with CancellationToken wired to the engine interrupt:
using var result = await conn.QueryAsync("MATCH (p:Person) RETURN p.name", cancellationToken);
await foreach (var tuple in conn.StreamAsync("MATCH (p:Person) RETURN p.name", cancellationToken))
using (tuple)
Console.WriteLine(tuple.GetValue(0).GetValue());Also: QueryAllAsync, PrepareAsync, ExecuteAsync. A single Connection serializes operations, so
use one connection per concurrent operation (or the connection pool guidance in LadybugDB.Extensions).
PreparedStatement.Bind(...) covers all scalar types plus decimal/LadybugDecimal, BigInteger
(INT128), byte[] (BLOB, top-level), IReadOnlyDictionary<string,object?> (STRUCT), and BindMap
(MAP). DECIMAL reads return a real decimal when it fits and a lossless LadybugDecimal otherwise —
never a bare string.
For a hot loop that runs the same Cypher with different parameters (bulk insert/update, by-key
delete, scalar-per-key reads), Connection.ExecuteMany prepares the statement once and re-binds
each parameter set on it — so the query is planned a single time and the pooled-bind fast path runs
on every iteration. Each QueryResult is disposed for you. An empty sequence is a no-op.
// Write path: one prepare, N executes; results disposed internally.
conn.ExecuteMany(
"CREATE (:Person {name: $name, age: $age})",
people.Select(p => new Dictionary<string, object?> { ["name"] = p.Name, ["age"] = p.Age }));
// Read path: project one value per parameter set, in input order.
IReadOnlyList<long?> ages = conn.ExecuteMany(
"MATCH (p:Person) WHERE p.name = $name RETURN p.age",
names.Select(n => new Dictionary<string, object?> { ["name"] = n }),
r => (long?)r.Rows().FirstOrDefault()?[0]);
// Async equivalents honor a CancellationToken: ExecuteManyAsync(...) / ExecuteManyAsync(..., selector, ct).Ladybug extensions (e.g. json, fts, vector, httpfs) install and load through the query engine.
The binding adds convenience helpers and — importantly — loads the native engine with global symbol
visibility on Linux/macOS so a dynamically loaded extension resolves the engine's symbols:
conn.InstallExtension("json"); // INSTALL json
conn.LoadExtension("json"); // LOAD EXTENSION jsonusing LadybugDB.Arrow;
using var r = conn.Query("MATCH (p:Person) RETURN p.name, p.age");
foreach (RecordBatch batch in r.ReadBatches()) // zero-copy export
Console.WriteLine($"{batch.Length} rows");
conn.CreateArrowTable("People", someRecordBatch); // ingest a RecordBatch as a node tableservices.AddSingleton(_ => new Connection(database));
services.AddSingleton<ILadybugExecutor>(sp => new LadybugConnectionExecutor(sp.GetRequiredService<Connection>()));
services.AddLadybug(o => { o.DatabasePath = "./app.db"; o.MaxThreads = 4; });
services.AddLadybugResilience(); // timeout + retry + circuit breaker (no Polly)
services.AddLadybugHealthCheck(); // ASP.NET Core health check
// Stream typed rows:
await foreach (var row in executor.StreamAsync("MATCH (p:Person) RETURN p.name AS name"))
Console.WriteLine(row.Get<string>("name"));
// Export a QueryResult:
string json = result.ToJson();
string csv = result.ToCsv();
DataTable table = result.ToDataTable();The core binding emits an ActivitySource and Meter named LadybugDB (instruments
db.query.count, db.query.errors, db.query.duration.ms). They are zero-cost when no listener is
attached; subscribe via your OpenTelemetry pipeline to observe query activity.
Annotate a record/class with [LadybugRow] and map result rows with a reflection-free, AOT-safe
generated mapper:
using LadybugDB.Mapping;
[LadybugRow]
public sealed record Person(string Name, long Age);
foreach (Person p in result.Map<Person>()) // generated; also MapAsync<Person>()
Console.WriteLine($"{p.Name} ({p.Age})");dotnet build LadybugDB.slnx -c Release
dotnet test LadybugDB.slnx -c ReleaseThe binding needs the native Ladybug shared library at runtime (lbug_shared.dll /
liblbug.so / liblbug.dylib). When the native library is not available, native round-trip tests skip
(the ABI/struct-layout guards still run). Set LADYBUG_REQUIRE_NATIVE=1 to require a native load.
This repo does not contain the engine source; the native libraries come from the upstream
Ladybug engine. Packaging is driven by a Cake Frosting build
project under cake/ (run it with the build.ps1 / build.sh bootstrap):
./build.sh --target Test # build + stage the host native + run the suite
./build.sh --target Pack # build the full package family into ./artifactsAll packages in the family share one version. The first three numeric segments track the upstream engine
release, and the optional fourth segment is the .NET package revision for binding-only releases. For
example, package 0.17.1.0 wraps the Ladybug v0.17.1 engine; a future binding-only fix over the same
engine would be 0.17.1.1. Prerelease suffixes are reserved for preview builds.
The package version is defined once in version.txt at the repo root. Override it with
--package-version <v> (the release workflow uses the git tag). The engine release defaults to the first
three numeric package-version segments; override with --engine-version when needed.
Packstages the prebuiltliblbug-*assets for every shipped RID (downloaded from an upstreamLadybugDB/ladybugGitHub Release, pinned to the engine version derived fromversion.txt), packsLadybugDB,LadybugDB.Extensions,LadybugDB.Arrow, oneLadybugDB.Native.<rid>package per RID, and theLadybugDB.Nativemeta-package, then verifies every package's contents.- CI / release (
.github/workflows/) invoke the same pipeline; the release workflow gates on the linux-x64 suite against the real engine and publishes all packages to nuget.org via OIDC. - Local development is easiest when this repo is checked out as the
tools/csharp_apisubmodule inside the Ladybug monorepo:scripts/build-native-and-test.ps1buildslbug_sharedfrom the parent engine tree, stages it intolib/runtimes/win-x64/native/, and runs the suite. With a native already staged there,--target Testreuses it instead of downloading.
See MAINTAINING.md for the maintainer guide (versioning, ABI updates, release flow).