Async Operations & Locking
Since the graph database handles high-concurrency workloads, many operations are asynchronous to avoid blocking threads.
Async Enumeration
If you are processing a large result set, consider using AsEnumerableAsync(). This method parallelizes the reading of data from disk, which can significantly improve performance for heavy queries. AsEnumerableWithEdgesAsync() does the same but materializes each node's edges as well.
await foreach (var node in Graph.Query().StartAt("Log").AsEnumerableAsync())
{
// Process one by one without buffering everything in memory
await ProcessLogAsync(node);
}
Cancelling enumeration
Both AsEnumerableAsync() and AsEnumerableWithEdgesAsync() accept a CancellationToken. The token also flows through automatically when you attach it with WithCancellation(token) on the await foreach, so the enumeration stops promptly once the token is cancelled:
await foreach (var node in Graph.Query()
.StartAt("Log")
.AsEnumerableAsync()
.WithCancellation(cancellationToken))
{
await ProcessLogAsync(node);
}
Composing Async Query Pipelines
A few query entry points are asynchronous — StartSearchAsync(...), OutManyAsync(...), and StartAtSimilarTextAsync(...) return a Task<IQuery>, which forces an await mid-chain. The Then(...) extensions let you chain the remaining sync and async steps and await the pipeline once:
var hits = await Graph.Query()
.StartAtSimilarTextAsync("battery drains overnight", count: 20, nodeTypes: ["SupportCase"])
.Then(q => q.IsRelatedTo(deviceUID)) // sync step
.Then(q => q.OutManyAsync(2, ["Part"])) // async step
.Then(q => q.ToList()); // terminal projection
See Querying the Graph and the Graph Query Language reference for the full method list.
Locking and Thread Safety
When modifying the graph, you must use async methods to acquire locks. This ensures thread safety and prevents data corruption.
The Locking Pattern
To modify a node, you must follow this specific pattern:
- Acquire Lock: Use
TryGetLockedAsyncorGetOrAddLockedAsyncto get aLockedNode. - Modify: Perform your updates on the
LockedNodeobject. - Commit: Call
CommitAsyncto save changes and release the lock.
Important
Locks are exclusive. While you hold a lock on a node, no other thread can read or write to it. Keep your critical sections (the time between lock and commit) as short as possible.
// 1. Acquire Lock
var lockedNode = await Graph.TryGetLockedAsync(someUID);
if (lockedNode != null)
{
try
{
// 2. Modify
// Perform logic that might throw exceptions here
lockedNode.UpdateProperty("visited", true);
// 3. Commit
await Graph.CommitAsync(lockedNode);
}
catch (Exception)
{
// If something goes wrong, abandon changes to release the lock
Graph.AbandonChanges(lockedNode);
throw;
}
}
LockedNode vs ReadOnlyNode
- ReadOnlyNode: Lightweight, safe for reading. Returned by queries.
- LockedNode: Heavyweight, represents exclusive access to a node for modification.
CommitAsync
The CommitAsync method pushes changes to the storage engine and releases the lock.
// Commit single node
await Graph.CommitAsync(node);
// Commit multiple nodes (atomically)
await Graph.CommitAsync(nodeA, nodeB);
If you modify a node but decide not to save (e.g., validation failed), use AbandonChanges(node).