Skip to content

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
// 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.