Graph
The Mosaik.GraphDB.Safe.Graph class (often referred to simply as Graph) is the primary way to interact with the Curiosity Graph Database in custom code. It wraps the low-level GraphDB instance to provide thread safety and, most importantly, mechanisms to prevent deadlocks when modifying the graph.
Purpose
Curiosity's graph database is highly concurrent. When multiple operations attempt to modify the same nodes simultaneously, there is a risk of deadlocks (e.g., Process A locks Node 1 and waits for Node 2, while Process B locks Node 2 and waits for Node 1).
Graph enforces patterns that help avoid these situations or detect them early.
Key Methods
Creating and Locking Nodes
To modify a node (update properties, add edges), you must first acquire a lock on it. You can retrieve an existing node or create a new one.
1. Standard Node (Deterministic Key) Use this when you have a natural key (e.g., product code, email, username) and want to ensure the node is unique for that key.
// Get or create a node with a specific Type and Key
var productNode = await Graph.GetOrAddLockedAsync("Product", "P-12345");
// Set properties
productNode.SetString("Name", "Super Gadget");
productNode.SetDecimal("Price", 99.99m);
2. Internal Node (Generated Key) Use this when you need a node but don't have a natural key (e.g., a "Transaction" or "Log" where ID doesn't matter).
// Create a node with a random GUID key
var logNode = await Graph.GetOrAddLockedAsync("LogEntry", Guid.NewGuid().ToString());
logNode.SetString("Message", "Operation started");
logNode.SetTime("Timestamp", Time.Now);
3. Lock Existing Node Use this when you only want to update a node if it already exists.
// Try to get an existing node and lock it (returns null if not found)
var lockedNode = await Graph.TryGetLockedAsync(someUID);
if (lockedNode != null)
{
// Update...
}
Managing Edges
Edges are managed through the LockedNode instance. You can add unique edges (recommended), standard edges (allows duplicates), or remove them.
Adding Edges
// 1. Link to another LockedNode (safest and easiest)
var (product, category) = await Graph.GetOrAddLockedAsync("Product", "P-12345", "Category", "Electronics");
product.AddUniqueEdge("CATEGORY", category);
// 2. Link to a target by UID (if you don't need to lock the target)
// You need the target's UID and its numeric Type UID.
var manufacturerUID = Node.GetUID(N.Manufacturer.Type, "AcmeCorp");
var manufacturerTypeUID = Graph.GetNodeTypeUID("Manufacturer");
product.AddUniqueEdge("MANUFACTURED_BY", manufacturerUID, manufacturerTypeUID);
Removing Edges
// Remove a specific edge type to a target
product.RemoveUniqueEdge("CATEGORY", category);
// Remove all edges of any type to a target
product.RemoveAllEdgesTo(category.UID);
Committing and Abandoning Changes
Once you have modified a LockedNode, you must commit the changes to release the lock and persist the data. If an error occurs, you must abandon the changes to release the lock immediately.
Recommended Pattern
var node = await Graph.GetOrAddLockedAsync("Product", "P-12345");
try
{
// Perform updates
node.SetString("Status", "InStock");
node.AddUniqueEdge("STORED_IN", warehouseUID, warehouseTypeUID);
// Commit changes
await Graph.CommitAsync(node);
}
catch (Exception ex)
{
// Release the lock without saving
Graph.AbandonChanges(node);
throw; // Re-throw if needed
}
Batch Operations
When you need to perform many small updates to a single node (perhaps from different parts of your logic) without committing immediately each time, you can use batching.
The Batch method queues an action to be performed on a node. The operations are automatically grouped by UID, ensuring that when the batch is committed, all operations for a specific node are applied in a single lock-update-commit cycle.
// Queue an operation (does not execute immediately)
Graph.Batch(someUID, (lockedNode) => {
lockedNode.UpdateProperty("status", "active");
});
Graph.Batch(someUID, (lockedNode) => {
lockedNode.AddEdge("processed_by", userUID);
});
// MANDATORY: Commit all batched operations
// This will lock nodes one by one, apply all queued actions, and commit.
await Graph.CommitBatchAsync();
Important
If you do not call CommitBatchAsync(), none of the batched operations will be executed.
Merging Nodes
To deduplicate two nodes that represent the same real-world entity, merge one into the other. All edges on the node being deleted are re-pointed to the node being kept, then the deleted node is removed.
// Merge `duplicateUID` into `canonicalUID`: edges move to canonicalUID, duplicateUID is deleted
await Graph.MergeNodeAsync(toKeep: canonicalUID, toDelete: duplicateUID);
Use MergeNodeAsAliasAsync instead when you also want the deleted node's key (or another field) preserved as a searchable alias on the surviving node:
await Graph.MergeNodeAsAliasAsync(
toKeep: canonicalUID,
toDelete: duplicateUID,
possibleFieldsForAlias: new[] { "Name", "Key" },
language: Language.English,
ignoreCase: true);
Both methods accept ignoreAlreadyExistingEdges (default true) to silently skip edges that would duplicate an edge already present on the kept node.
Read Operations
Graph exposes all standard reading methods.
Get(uid)/TryGet(uid, out node): Retrieve read-only nodes.GetWithEdges(uid): Retrieve node with edge data.Query(): Start a query.
Rendering node labels
Graph.Labels turns a label definition into the string a client should display for a node. It accepts both label syntaxes — a fallback list (Title|Subject|Name) and an interpolated label ($Order {OrderId} for {Customer}) — and uses the same parser the front-end and the schema controller use, so a label rendered from custom code matches the one the UI shows. The definition syntax and the format specifiers are documented in Node renderers.
Every rendering method takes an optional userUID. Pass one and field-level access control is applied while the placeholder values are read: a field the user is not allowed to read renders as BLOCKED instead of its value. Omit it and no check runs — reserve that for callers that already bypass access filtering, such as a system-admin-only endpoint or a scheduled task with no user context.
// Render the label configured for the node's own type, as this user may see it
var label = Graph.Labels.RenderForNode(nodeUID, userUID: user.UID);
// Render an explicit label definition against a node you already loaded
var title = Graph.Labels.Render("$Order {OrderId} for {Customer}", node, userUID: user.UID);
// No user passed, so no field-level check runs
var raw = Graph.Labels.RenderForNode(nodeUID);
| Member | Returns | Notes |
|---|---|---|
Render(labelDefinition, node, userUID = default) |
string |
Renders the definition against a node you already loaded. Empty when the node is null. |
Render(labelDefinition, nodeUID, userUID = default) |
string |
Loads the node first. Empty when it does not exist. |
RenderForNode(node, userUID = default) |
string |
Uses the label definition configured for the node's own type. Empty when it has none. |
RenderForNode(nodeUID, userUID = default) |
string |
Same, starting from a UID. |
GetLabelDefinition(nodeType) |
string |
The LabelField configured for that type, or null when it has none. |
IsInterpolated(labelDefinition) |
bool |
true for a $...{...} definition, false for a fallback list. |
GetReferencedFields(labelDefinition) |
string[] |
The fields the definition reads — use it to fetch only the values a label needs. |
BlockedFieldMarker |
string |
The text substituted for a field the user cannot read (BLOCKED). |
A restricted field renders as BLOCKED rather than as an empty value on purpose: in a fallback list any non-empty value ends the fallback, so a blocked first field cannot cascade to a visible secondary field and reveal which fields are restricted.
Labels is declared on IReadOnlyGraph, so it is reachable from anything holding a read-only graph — custom endpoints, scheduled tasks, custom code indexes, and migrations.
Best Practices
- Always Commit: Ensure every
GetOrAddLockedAsyncis paired with aCommitAsync(orAbandonChangesin error cases). - Order of Locks: If locking multiple nodes, try to always lock them in a consistent order (e.g., by UID) to reduce deadlock risk.
- Short Critical Sections: Keep the time between acquiring a lock and committing it as short as possible. Do expensive computations before locking if possible.