Curiosity

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:

  1. Acquire Lock: Use TryGetLockedAsync or GetOrAddLockedAsync to get a LockedNode.
  2. Modify: Perform your updates on the LockedNode object.
  3. Commit: Call CommitAsync to 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).

Referenced by

© 2026 Curiosity. All rights reserved.