C# / .NET API Reference
Complete reference for the Grafeo NuGet package, .NET 8 bindings that wrap the grafeo-c native library via P/Invoke.
Installation
dotnet add package Grafeo
The NuGet package bundles pre-built native libraries for Windows (x64), Linux (x64, arm64) and macOS (x64, arm64), built with every feature of the C binding (the all variant of the prebuilt libraries), so every query language works. No separate native install is required.
Quick Start
using Grafeo;
using var db = GrafeoDB.Memory();
db.Execute("INSERT (:Person {name: 'Alix', age: 30})");
db.Execute("INSERT (:Person {name: 'Gus', age: 25})");
db.Execute("INSERT (:Person {name: 'Alix'})-[:KNOWS]->(:Person {name: 'Gus'})");
var result = db.Execute("MATCH (p:Person) RETURN p.name, p.age");
foreach (var row in result.Rows)
{
Console.WriteLine($"{row["p.name"]}: {row["p.age"]}");
}
Console.WriteLine($"Nodes: {db.NodeCount}, Edges: {db.EdgeCount}");
GrafeoDB Class
The primary database handle. Thread-safe (the Rust engine uses Arc<RwLock>). Implements IDisposable and IAsyncDisposable for deterministic cleanup.
Lifecycle
// In-memory database
using var db = GrafeoDB.Memory();
// Persistent database (opens or creates at path)
using var db = GrafeoDB.Open("/path/to/data");
// Async disposal
await using var db = GrafeoDB.Memory();
// Library version
Console.WriteLine(GrafeoDB.Version);
| Member | Returns | Description |
GrafeoDB.Memory() | GrafeoDB | Create an in-memory database |
GrafeoDB.Open(string path) | GrafeoDB | Open or create a persistent database at path |
Dispose() | void | Free the native database handle |
DisposeAsync() | ValueTask | Async disposal (calls Dispose synchronously) |
Version | string | Static property returning the Grafeo library version |
Query Execution (Sync)
// GQL (ISO standard)
var result = db.Execute("MATCH (p:Person) RETURN p.name");
// GQL with parameters
var result = db.ExecuteWithParams(
"MATCH (p:Person) WHERE p.name = $name RETURN p",
new Dictionary<string, object?> { ["name"] = "Alix" });
// Other query languages
var result = db.ExecuteCypher("MATCH (p:Person) RETURN p.name");
var result = db.ExecuteSparql("SELECT ?s WHERE { ?s a <http://example.org/Person> }");
var result = db.ExecuteGremlin("g.V().hasLabel('Person').values('name')");
var result = db.ExecuteGraphql("{ persons { name } }");
var result = db.ExecuteSql("SELECT p.name FROM GRAPH_TABLE (g MATCH (p:Person)) AS t");
| Method | Returns | Description |
Execute(string query) | QueryResult | Execute a GQL query |
ExecuteWithParams(string query, Dictionary<string, object?> parameters) | QueryResult | Execute a GQL query with parameter binding |
ExecuteCypher(string query) | QueryResult | Execute a Cypher query |
ExecuteSparql(string query) | QueryResult | Execute a SPARQL query |
ExecuteGremlin(string query) | QueryResult | Execute a Gremlin query |
ExecuteGraphql(string query) | QueryResult | Execute a GraphQL query |
ExecuteSql(string query) | QueryResult | Execute a SQL/PGQ query |
Query Execution (Async)
All sync query methods have async counterparts that run on the thread pool. Each accepts an optional CancellationToken.
var result = await db.ExecuteAsync("MATCH (p:Person) RETURN p.name");
var result = await db.ExecuteWithParamsAsync(
"MATCH (p:Person) WHERE p.age > $min RETURN p.name",
new Dictionary<string, object?> { ["min"] = 18 });
var result = await db.ExecuteCypherAsync("MATCH (p:Person) RETURN p", ct);
var result = await db.ExecuteSparqlAsync(query, ct);
var result = await db.ExecuteGremlinAsync(query, ct);
var result = await db.ExecuteGraphqlAsync(query, ct);
var result = await db.ExecuteSqlAsync(query, ct);
| Method | Returns | Description |
ExecuteAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute GQL on the thread pool |
ExecuteWithParamsAsync(string query, Dictionary<string, object?> parameters, CancellationToken ct = default) | Task<QueryResult> | Execute GQL with params on the thread pool |
ExecuteCypherAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute Cypher on the thread pool |
ExecuteSparqlAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute SPARQL on the thread pool |
ExecuteGremlinAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute Gremlin on the thread pool |
ExecuteGraphqlAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute GraphQL on the thread pool |
ExecuteSqlAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute SQL/PGQ on the thread pool |
Transactions
using var tx = db.BeginTransaction();
tx.Execute("INSERT (:Person {name: 'Alix'})");
tx.Execute("INSERT (:Person {name: 'Gus'})");
tx.Commit();
// With isolation level
using var tx = db.BeginTransaction("read_committed");
| Method | Returns | Description |
BeginTransaction() | Transaction | Begin with default isolation (snapshot) |
BeginTransaction(string) | Transaction | Begin with isolation level: "read_committed", "snapshot"; "serializable" is refused for now: snapshot isolation is the strongest level until queries record their reads |
BeginTransaction(IsolationLevel) | Transaction | Type-safe overload: IsolationLevel.ReadCommitted, .Snapshot (.Serializable is refused for now) |
Node CRUD
// Create a node with labels and properties
long nodeId = db.CreateNode(
["Person"],
new Dictionary<string, object?> { ["name"] = "Alix", ["age"] = 30 });
// Get a node by ID (returns null if not found)
Node? node = db.GetNode(nodeId);
// Modify properties
db.SetNodeProperty(nodeId, "city", "Amsterdam");
db.RemoveNodeProperty(nodeId, "city");
// Modify labels
db.AddNodeLabel(nodeId, "Employee");
db.RemoveNodeLabel(nodeId, "Employee");
// Delete
bool deleted = db.DeleteNode(nodeId);
| Method | Returns | Description |
CreateNode(IEnumerable<string> labels, Dictionary<string, object?>? properties = null) | long | Create a node, returns the new node ID |
GetNode(long id) | Node? | Get a node by ID, or null if not found |
DeleteNode(long id) | bool | Delete a node, returns true if deleted |
SetNodeProperty(long id, string key, object? value) | void | Set a property on a node |
RemoveNodeProperty(long id, string key) | bool | Remove a property, returns true if removed |
AddNodeLabel(long id, string label) | bool | Add a label, returns true if added |
RemoveNodeLabel(long id, string label) | bool | Remove a label, returns true if removed |
Edge CRUD
long alixId = db.CreateNode(["Person"], new() { ["name"] = "Alix" });
long gusId = db.CreateNode(["Person"], new() { ["name"] = "Gus" });
// Create an edge with type and optional properties
long edgeId = db.CreateEdge(alixId, gusId, "KNOWS",
new Dictionary<string, object?> { ["since"] = 2024 });
// Get an edge by ID
Edge? edge = db.GetEdge(edgeId);
// Modify properties
db.SetEdgeProperty(edgeId, "weight", 0.9);
db.RemoveEdgeProperty(edgeId, "weight");
// Delete
bool deleted = db.DeleteEdge(edgeId);
| Method | Returns | Description |
CreateEdge(long sourceId, long targetId, string edgeType, Dictionary<string, object?>? properties = null) | long | Create an edge, returns the new edge ID |
GetEdge(long id) | Edge? | Get an edge by ID, or null if not found |
DeleteEdge(long id) | bool | Delete an edge, returns true if deleted |
GetNodeEdges(long nodeId, Direction direction = Direction.Out, IEnumerable<string>? edgeTypes = null) | IReadOnlyList<NodeEdge> | A node's edges without a query, as (NeighborId, EdgeId); Direction.Both lists a self-loop once |
SetEdgeProperty(long id, string key, object? value) | void | Set a property on an edge |
RemoveEdgeProperty(long id, string key) | bool | Remove a property, returns true if removed |
Batches and Upserts
// One transaction each; IDs in input order
var ids = db.BatchCreateNodesWithProps(["Person"], [
new Dictionary<string, object?> { ["name"] = "Alix" },
new Dictionary<string, object?> { ["name"] = "Gus" },
]);
var edgeIds = db.BatchCreateEdges([new BatchEdge(ids[0], ids[1], "KNOWS")]);
// Create or update by a key property
UpsertSummary summary = db.UpsertNodes(["File"],
[new Dictionary<string, object?> { ["path"] = "a.rs" }],
new UpsertNodesOptions { Key = "path" });
summary = db.UpsertEdges("IMPORTS",
[new Dictionary<string, object?> { ["id"] = "a-b", ["src"] = "a.rs", ["dst"] = "b.rs" }],
new UpsertEdgesOptions { EndpointKey = "path", EndpointLabels = ["File"] });
| Method | Returns | Description |
BatchCreateNodesWithProps(IEnumerable<string> labels, IEnumerable<IDictionary<string, object?>> rows) | IReadOnlyList<long> | One node per row, in one transaction |
BatchCreateEdges(IEnumerable<BatchEdge> edges) | IReadOnlyList<long> | The edges in one transaction; an edge to a missing node fails the batch |
UpsertNodes(labels, rows, UpsertNodesOptions? options = null) | UpsertSummary | Create or update nodes matched by the key and all labels; rows without the key are skipped |
UpsertEdges(string edgeType, rows, UpsertEdgesOptions? options = null) | UpsertSummary | Create or update edges between existing endpoints; a row whose endpoint matches no node or several is skipped |
Named Graphs
db.Graph(name) returns a GrafeoGraph that works in one named graph without switching the current graph: Execute, ExecuteWithParams, ExecuteLanguage, the node and edge methods (GetNode and GetEdge throw when the entity does not exist), the batches and the upserts. Once the graph is dropped, every call throws. Dispose frees it.
using var trips = db.Graph("trips");
long prague = trips.CreateNode(["City"], new Dictionary<string, object?> { ["name"] = "Prague" });
var result = trips.Execute("MATCH (c:City) RETURN c.name");
Time Travel
Reads of the database as it was at an earlier epoch or time. GrafeoDB.Memory(HistoryRetention) and GrafeoDB.Open(path, HistoryRetention) set how much history it keeps: HistoryRetention.Off (the default), HistoryRetention.Forever or HistoryRetention.For(TimeSpan); a database file stores the setting. A point is a TimePoint: TimePoint.Epoch(ulong), TimePoint.At(DateTimeOffset) (the last commit at or before it) or TimePoint.Ago(TimeSpan). A point before the history the database keeps throws GrafeoException ("not retained").
| Member | Returns | Description |
CurrentEpoch | ulong | The epoch of the last commit |
EpochAsOf(TimePoint point) | ulong | The epoch a point reads at |
AsOf(TimePoint point) | AsOfView | A read-only view at a point: Epoch, Execute, ExecuteWithParams, GetNode, GetEdge and GetNodeProperty (null when the entity or property did not exist then); it keeps the history of its epoch until disposed |
ExecuteAsOf(string query, TimePoint point, Dictionary<string, object?>? parameters = null) | QueryResult | One query at a point |
using var db = GrafeoDB.Memory(HistoryRetention.For(TimeSpan.FromDays(7)));
using var view = db.AsOf(TimePoint.Ago(TimeSpan.FromMinutes(5)));
var result = view.Execute("MATCH (p:Person) RETURN count(p) AS n");
Change History
With db.CdcEnabled = true, every committed change is recorded as a ChangeEvent (kind, epoch, properties before and after, labels, the endpoints of an edge, and DeletedWith on an edge a detach delete removed).
| Method | Returns | Description |
NodeHistory(long id) | IReadOnlyList<ChangeEvent> | Every change of a node, oldest first |
EdgeHistory(long id) | IReadOnlyList<ChangeEvent> | Every change of an edge, oldest first |
NodeHistorySince(long id, ulong sinceEpoch) | IReadOnlyList<ChangeEvent> | The changes of a node from an epoch on |
ChangesBetween(ulong startEpoch, ulong endEpoch) | IReadOnlyList<ChangeEvent> | Every change in an epoch range, in commit order |
Stats and Info
long nodes = db.NodeCount;
long edges = db.EdgeCount;
IReadOnlyDictionary<string, object?> info = db.Info();
Console.WriteLine(info["version"]);
| Member | Returns | Description |
NodeCount | long | Number of nodes in the database |
EdgeCount | long | Number of edges in the database |
Info() | IReadOnlyDictionary<string, object?> | Database metadata (version, node count, edge count, etc.) |
Persistence
db.Save("/path/to/backup.grafeo");
| Method | Returns | Description |
Save(string path) | void | Save the database to a file |
Vector Search
// Create a vector index via GQL
db.Execute("CREATE VECTOR INDEX document_embedding ON :Document(embedding) DIMENSION 384");
// Similarity search: returns nearest k nodes
float[] queryVector = GetEmbedding("graph database");
IReadOnlyList<VectorResult> results = db.VectorSearch(
"Document", "embedding", queryVector, k: 10, ef: 64);
foreach (var hit in results)
{
Console.WriteLine($"Node {hit.NodeId}, distance: {hit.Distance}");
}
// MMR search: balances relevance with diversity
IReadOnlyList<VectorResult> diverse = db.MmrSearch(
"Document", "embedding", queryVector,
k: 10, fetchK: 50, lambda: 0.7f, ef: 64);
// Index management
db.RebuildVectorIndex("Document", "embedding");
db.DropVectorIndex("Document", "embedding");
| Method | Returns | Description |
VectorSearch(string label, string property, float[] query, int k, uint ef = 0) | IReadOnlyList<VectorResult> | k-NN similarity search ordered by distance |
MmrSearch(string label, string property, float[] query, int k, int fetchK, float lambda, int ef = 0) | IReadOnlyList<VectorResult> | Maximal Marginal Relevance search for diverse results |
DropVectorIndex(string label, string property) | bool | Drop a vector index. Returns true if dropped, false if not found |
RebuildVectorIndex(string label, string property) | void | Rebuild a vector index |
BatchVectorSearch(string label, string property, IReadOnlyList<float[]> queries, int k, int ef = 0, Dictionary<string, object?>? filters = null) | IReadOnlyList<IReadOnlyList<VectorResult>> | Several queries of one dimension at once, with optional property filters |
Transaction Class
An ACID transaction handle. Implements IDisposable and IAsyncDisposable. If Commit() is not called before disposal, the transaction is automatically rolled back.
using var tx = db.BeginTransaction();
tx.Execute("INSERT (:Person {name: 'Alix'})");
tx.Execute("INSERT (:Person {name: 'Gus'})");
var result = tx.ExecuteWithParams(
"MATCH (p:Person) WHERE p.name = $name RETURN p",
new Dictionary<string, object?> { ["name"] = "Alix" });
tx.Commit(); // if this line is not reached, the transaction auto-rolls back
| Method | Returns | Description |
Execute(string query) | QueryResult | Execute a GQL query within the transaction |
ExecuteAsync(string query, CancellationToken ct = default) | Task<QueryResult> | Execute GQL on the thread pool within the transaction |
ExecuteWithParams(string query, Dictionary<string, object?> parameters) | QueryResult | Execute GQL with params within the transaction |
ExecuteWithParamsAsync(string query, Dictionary<string, object?> parameters, CancellationToken ct = default) | Task<QueryResult> | Execute GQL with params on the thread pool |
Commit() | void | Commit the transaction, making all changes permanent |
Rollback() | void | Roll back the transaction, discarding all changes. Throws TransactionException after a commit that succeeded; does nothing after one that failed, which ended the transaction |
Dispose() | void | Dispose the handle; auto-rolls back if not committed |
DisposeAsync() | ValueTask | Async disposal (calls Dispose synchronously) |
Data Types
All entity types are immutable C# records.
GrafeoDecimal
An exact decimal from Grafeo (GQL 1.50, CAST(x AS DECIMAL(10, 2))): up to 38 digits, kept as its Text with its scale. Equal values are equal whatever their scale (1.50 equals 1.5). ToDecimal() and TryToDecimal give a .NET decimal (which holds 28 to 29 digits), ToDouble() the nearest double, and GrafeoDecimal.FromDecimal(value) or new GrafeoDecimal("1.50") make one to pass as a parameter, exactly.
var result = db.Execute("RETURN 19.99 AS price");
var price = (GrafeoDecimal)result.Rows[0]["price"];
Console.WriteLine(price.Text); // 19.99
decimal exact = price.ToDecimal(); // 19.99m
GrafeoTime and GrafeoDuration
A GQL TIME with a UTC offset (12:30:00+02:00), or one finer than 100 ns, comes back as a GrafeoTime: Hour, Minute, Second, Nanosecond, Offset (null for a local time), Text, and ToTimeOnly() for the clock reading. Any other TIME comes back as a TimeOnly. A GQL DURATION comes back as a GrafeoDuration of Months, Days and Nanoseconds, kept apart because a month and a day have no fixed length (P1M is not P30D, P1D is not PT24H); TryToTimeSpan and ToTimeSpan() give a TimeSpan when there are no months, the nanoseconds are whole 100 ns ticks and the value fits in a TimeSpan (otherwise TryToTimeSpan returns false and ToTimeSpan() throws). Make one with new GrafeoTime(12, 30, 0, offset: TimeSpan.FromHours(2)), new GrafeoTime("12:30:00+02:00"), new GrafeoDuration(14, 3, 0) or new GrafeoDuration("P1Y2M3D"); passed as a parameter, each is that TIME or DURATION.
var row = db.Execute("RETURN TIME '12:30:00+02:00' AS opens, DURATION 'P1M' AS term").Rows[0];
var opens = (GrafeoTime)row["opens"]!; // 12:30:00+02:00, Offset 02:00:00
var term = (GrafeoDuration)row["term"]!; // Months 1
RdfTerm
An RDF term from a SPARQL result: its Type (RdfTermType.Iri, BlankNode or Literal), its Value (the IRI, the blank node's label without _:, or the lexical form), and for a literal its Language tag or Datatype (null for a simple literal; LiteralDatatype gives xsd:string or rdf:langString there). An RdfTerm passed as a parameter, or stored as a property, is that exact term. Language tags compare without case, so a term keeps its tag in lowercase (Language: "FR" is "fr"), and a literal of datatype xsd:string is the simple literal: two RdfTerms are equal when they are the same RDF term. A dictionary with type and value keys stays a dictionary: on the wire a term is the $term tag (see the C API). A result's Kind is ResultKind.Rows, ResultKind.Boolean for a SPARQL ASK (Boolean gives its answer) or ResultKind.Graph for CONSTRUCT and DESCRIBE.
var result = db.ExecuteSparql("SELECT ?o WHERE { <http://ex.org/apple> <http://ex.org/label> ?o }");
var label = (RdfTerm)result.Rows[0]["o"];
Console.WriteLine($"{label.Value} {label.Language}"); // Pomme fr
Node
public sealed record Node(
long Id,
IReadOnlyList<string> Labels,
IReadOnlyDictionary<string, object?> Properties);
Edge
public sealed record Edge(
long Id,
string Type,
long SourceId,
long TargetId,
IReadOnlyDictionary<string, object?> Properties);
QueryResult
public sealed record QueryResult(
IReadOnlyList<string> Columns,
IReadOnlyList<IReadOnlyDictionary<string, object?>> Rows,
IReadOnlyList<Node> Nodes,
IReadOnlyList<Edge> Edges,
double ExecutionTimeMs,
long RowsScanned);
| Field | Type | Description |
Columns | IReadOnlyList<string> | Column names from the RETURN clause |
Rows | IReadOnlyList<IReadOnlyDictionary<string, object?>> | Row data as column-name to value maps |
Nodes | IReadOnlyList<Node> | Node entities extracted from the result |
Edges | IReadOnlyList<Edge> | Edge entities extracted from the result |
ExecutionTimeMs | double | Query execution time in milliseconds |
RowsScanned | long | Number of rows scanned by the engine |
Counters | WriteCounters | What a write changed: NodesCreated, NodesDeleted, EdgesCreated, EdgesDeleted, PropertiesSet, LabelsAdded, LabelsRemoved (all zero for a read) |
VectorResult
public sealed record VectorResult(long NodeId, double Distance);
| Field | Type | Description |
NodeId | long | The matching node's ID |
Distance | double | Distance from the query vector |
Exception Hierarchy
All exceptions derive from GrafeoException, which carries a GrafeoStatus code from the native layer and the error's GQLSTATUS in GqlStatus ("22012" for a division by zero, see Error Codes).
GrafeoException (base, Status and GqlStatus properties)
├── QueryException (parse or execution errors)
├── TransactionException (commit, rollback, isolation errors)
├── StorageException (WAL, persistence, I/O errors)
└── SerializationException (JSON or value conversion errors)
try
{
db.Execute("INVALID QUERY");
}
catch (QueryException ex)
{
Console.WriteLine($"Query failed: {ex.Message}");
Console.WriteLine($"Status: {ex.Status}");
}
catch (GrafeoException ex)
{
Console.WriteLine($"Grafeo error [{ex.Status}]: {ex.Message}");
}
GrafeoStatus Enum
| Value | Name | Description |
| 0 | Ok | Success |
| 1 | Database | General database error |
| 2 | Query | Query parsing or execution error |
| 3 | Transaction | Transaction lifecycle error |
| 4 | Storage | Storage or persistence error |
| 5 | Io | I/O error |
| 6 | Serialization | JSON or value serialization error |
| 7 | Internal | Internal engine error |
| 8 | NullPointer | Null pointer passed to FFI |
| 9 | InvalidUtf8 | Invalid UTF-8 string passed to FFI |
Type Mapping
| C# Type | Grafeo Type | Notes |
null | Null | |
bool | Bool | |
int | Int64 | Widened to long |
long | Int64 | |
byte, sbyte, short, ushort, uint | Int64 | |
ulong, Int128, UInt128, BigInteger | Int64, or Decimal past 64 bits | Exact up to 38 digits (scale 0); past that an error naming the parameter |
float | Float64 | Widened to double |
double | Float64 | |
string | String | |
DateTime | Timestamp (LOCAL DATETIME) | To the microsecond, in UTC (an unspecified kind is read as local); comes back as a UTC DateTime |
DateTimeOffset | Zoned datetime | To the microsecond, with its offset; comes back as a DateTimeOffset |
DateOnly | Date | Comes back as a DateOnly |
TimeOnly | Time | A local time, to 100 ns; comes back as a TimeOnly |
GrafeoTime | Time | With an offset, or finer than 100 ns: comes back as a GrafeoTime |
TimeSpan, GrafeoDuration | Duration | A TimeSpan is a duration of its days and the rest; every duration comes back as a GrafeoDuration |
byte[] | Bytes | Base64-encoded for transport |
float[] | Vector | For embeddings and similarity search |
ReadOnlyMemory<float> | Vector | Zero-copy span-based vector input |
IList<object?> | List | Elements converted recursively |
IDictionary<string, object?> | Map | Keys must be strings |
A value read back goes in again as the type it came as.
Links