C# SDK reference
Curiosity.Library is the .NET client for a Curiosity Workspace. Use it from connectors, ingestion pipelines, scheduled tasks, and any external service that needs to talk to a workspace. This page lists the public surface area you actually use.
For declarative schemas, see Schema reference. For graph traversal, see Graph query language. For semantic retrieval, see Embeddings API.
Install
dotnet add package Curiosity.Library
Connect
using Curiosity.Library;
using var graph = Graph
.Connect(endpoint: "https://my-workspace.example.com",
token: Environment.GetEnvironmentVariable("CURIOSITY_TOKEN"),
connectorName: "my-connector")
.WithTimeout(TimeSpan.FromMinutes(5));
| Method | Notes |
|---|---|
Graph.Connect(endpoint, token, connectorName, …) |
Token-authenticated connection. token resolves via token scopes. |
Graph.Connect(endpoint, clientCertificate, …) |
Mutual-TLS connection. Use for hardened production environments. |
WithLoggingFactory(ILoggerFactory) |
Wire into Microsoft.Extensions.Logging. |
WithTimeout(TimeSpan) |
Per-request timeout. Defaults are generous; set this lower for tight ingestion windows. |
WithDryRun(bool) |
When true, commits are logged but not sent. Excellent for new-connector debugging. |
SetAutoCommitCost(everyNodes, everyEdges) |
Trigger an implicit commit after the local queue reaches the configured size. Defaults to no auto-commit. |
Graph implements IDisposable — wrap it in using so pending operations flush and the WebSocket closes cleanly.
Schemas
await graph.CreateNodeSchemaAsync<SupportCase>(overwrite: false);
await graph.CreateEdgeSchemaAsync("HasDevice", "AssignedTo");
| Method | Notes |
|---|---|
CreateNodeSchemaAsync<T>(overwrite) |
Declarative: builds the schema from [Node]/[Property]/etc. |
CreateNodeSchemaAsync(ISchema, overwrite) |
Fluent: pass a Schema.NewNode(...) definition. |
GetNodeSchemaAsync(nodeType) |
Inspect a registered schema. Returns the fields and key. |
CreateEdgeSchemaAsync(params string[] edges) |
Register edge types by name. |
CreateEdgeSchemaAsync(Type staticEdgesClass) |
Convenience: reflects public const string fields out of a class. |
overwrite: false is the safe default. Set true only during initial setup or controlled migrations — it replaces the existing schema.
Adding and updating nodes
The SDK builds a local write queue; nothing reaches the server until you commit.
var case_ = graph.AddOrUpdate(new SupportCase
{
Id = "TICKET-1042",
Summary = "Battery drains overnight",
Status = "Open",
OpenedAt = DateTimeOffset.UtcNow
});
var device = graph.TryAdd(new Device { Id = "MBA-2024" });
graph.Link(case_, device, "ForDevice");
await graph.CommitPendingAsync();
Single-node operations
| Method | Behaviour |
|---|---|
TryAdd<T>(T node) |
Insert if the key is new. Existing nodes are untouched. |
AddOrUpdate<T>(T node) |
Upsert. Existing properties are overwritten with the incoming values. |
Update<T>(T node) |
Update an existing node. Throws if the node does not exist. |
Delete<T>(T node) / Delete(Node) |
Mark for deletion. Edges to/from the node are removed. |
Non-generic overloads (AddOrUpdate(Node, …)) accept a free-form Node plus either object content (auto-mapped properties) or Action<NodeOperation> operation (explicit field-by-field control).
Ownership variants
Use these to write nodes with their permissions in one call.
| Method | Notes |
|---|---|
TryAddWithOwnership<T>(T, params Node[] owners) |
Insert + grant access to listed user/team nodes. |
AddOrUpdateWithOwnership<T>(T, params Node[] owners) |
Upsert + reset ownership to listed owners. |
AddOrUpdateWithOwnership(Node, object content, params Node[]) |
Upsert with content map + ownership. |
See Access control model.
Edges
graph.Link(case_, device, edgeType: "ForDevice");
graph.Link(case_, device, "ForDevice", "AppearsInCase"); // bidirectional
graph.Unlink(case_, device, "ForDevice");
graph.UnlinkExcept(case_, currentDevice, "ForDevice"); // keep only the current edge
| Method | Notes |
|---|---|
Link(from, to, edgeType, unique=true, ignoreMissingNodes=false, edgeTimestamp=null) |
Create a directional edge. |
Link(a, b, edgeAtoB, edgeBtoA, …) |
Create both directions in one call. |
Unlink(from, to, edgeType, unique=true) |
Remove an edge. |
Unlink(a, b, edgeAtoB, edgeBtoA, …) |
Remove both directions. |
UnlinkExcept(from, except, edgeType) |
Drop every edge of this type except the one to except. Handy for "single current owner" relationships. |
UnlinkExcept(from, except, edgeFwd, edgeRev) |
Bidirectional variant. |
unique: true prevents duplicate edges of the same type between the same pair. ignoreMissingNodes: true makes Link a no-op if either endpoint isn't in the graph — useful when partial ingestion can race.
Aliases
graph.AddAlias(case_, Language.English, "Battery issue", ignoreCase: true);
graph.ClearAliases(case_);
Aliases improve entity matching for free-text retrieval. See Entity capture.
Committing
The write queue holds nodes and edges in memory and ships them in batches. You must commit for changes to land.
graph.SetAutoCommitCost(everyNodes: 5_000, everyEdges: 10_000); // optional
// … writes …
await graph.CommitPendingAsync();
CommitPendingAsync flushes the queue. Always call it before disposing — using does not commit on its own. For long-running connectors, the auto-commit settings prevent unbounded queue growth.
Files
var fileNode = await graph.UploadFileAsync(stream,
fileName: "report.pdf",
sourceName: "intranet",
initiallyPrivate: true);
graph.MarkFileAsPrivate(fileNode);
| Method | Notes |
|---|---|
UploadFileAsync(string filePath, …) |
Upload from disk. |
UploadFileAsync(Stream, fileName, sourceName, checkHash, uri, initiallyPrivate) |
Upload from a stream. checkHash deduplicates by SHA-256. |
UploadFileAndPermissionsAsync(...) |
Upload + carry the source ACLs onto the file node. |
UploadFileToFolderAsync(...) |
Upload into a folder node (creates the structure if missing). |
UploadFileWithIdentifierAsync(...) |
Upload keyed by a stable external identifier rather than name. |
TryGetFileNodeAsync(fileName, sourceName, sha256Hash) |
Look up an existing file node. |
DeleteFileAsync(...) / DeleteFolderAsync(...) |
Tombstone files/folders. |
GetFileHash(Stream) |
Local SHA-256 helper. |
Users, teams, permissions
var team = await graph.CreateTeamAsync("Engineering");
var user = await graph.CreateUserAsync("alice", "alice@example.com", "Alice", "Smith");
graph.AddUserToTeam(user, team);
graph.RestrictAccessToTeam(case_, team);
| Method | Notes |
|---|---|
CreateUserAsync(userName, email, firstName, lastName, isActive=true, preferUserNameAsUniqueIdentifer=true, cache=false) |
Create or fetch a user node. |
CreateTeamAsync(teamName, description=null, cache=false) |
Create or fetch a team node. |
AddUserToTeam(user, team) / AddAdminToTeam(...) / RemoveUserFromTeam(...) |
Membership graph. |
RestrictAccessToTeam(node, team) |
Allow team members to see node. |
RestrictAccessToUser(node, user) |
Allow a single user to see node. |
ClearPermissions(node, exceptOwners=null) |
Remove all access except listed owners. |
InitializeAccessControlAsync(cachePath) / CacheAccessControlAsync() |
Bulk-load the ACL graph into a local cache before high-volume writes. |
Queries
var results = await graph.QueryAsync(q => q
.StartAt("SupportCase")
.SortByTimestamp(oldestFirst: false)
.Take(20)
.Emit("N"));
foreach (var node in results.Nodes("N")) { /* … */ }
The query builder mirrors what runs inside an endpoint. See Graph query language for the full method table.
Embeddings
await graph.ClearEmbeddingsIndexAsync(indexUID);
await graph.AddEmbeddingsToIndexAsync(indexUID, vectors, batchSize: 1_000);
Use these to push pre-computed vectors (mirrored from another store, or computed offline). See Embeddings API for query-time encoding.
Indexing lifecycle
await graph.PauseIndexing("bulk-load");
// … ingestion …
await graph.ResumeIndexing("bulk-load");
Pause indexing for the duration of a large backfill and resume when done. The pauseName lets concurrent pauses coexist; indexing only restarts when all pauses are released. See Reindexing and re-embedding.
Workspace export / import
await using (var stream = await graph.ExportWorkspaceDefinitionAsync())
{
// write to disk, version control, etc.
}
var importResult = await graph.ImportWorkspaceDefinitionsAsync(File.OpenRead("workspace.def"));
Definitions cover schemas, search indexes, endpoint metadata, and UI — not data. Use this to promote a workspace from dev to prod via your CI pipeline.
Logging
await graph.LogAsync(LogLevel.Information, "ingested 1042 cases");
LogAsync and LogManyAsync send entries to the workspace's central log store. They show up in the admin UI alongside system events.
WebSocket listener
graph.ListenToWebSocketMessages(async msg => { /* handle … */ });
Subscribe to push events from the workspace — used for keeping connector state in sync with UI-driven changes.
Mapping helpers
Dictionary<Node, string> idByEmail =
await graph.MapAsync(nodeType: "User", field: "Email", pageSize: 1_000);
MapAsync page-fetches every node of a type and projects out one field. Use it when you need an in-memory lookup table before bulk-linking.
Front-end deployment
await graph.UploadNewApplicationInterfaceAsync("./build", retries: 5);
Ship a Tesserae front-end bundle into the workspace. See Custom front-end.
Patterns and tips
- Always dispose.
Graphowns a HTTP client and a WebSocket. Leaking it leaks both. - Commit at boundaries, not in inner loops. Batch writes into the queue; commit when a logical unit is done. Pair with
SetAutoCommitCostfor high-volume runs. - Use
DryRunfor new connectors. It exercises every code path except the network write — perfect for development. - Cache the ACL graph for permission writes.
InitializeAccessControlAsyncbefore a bulk ingest avoids per-write lookups. - Pause indexing for big backfills. Resume after, then let the workspace rebuild once instead of fighting incremental rebuilds.