Downloadable open-source computer software from CVOYA. See the CVOYA software catalog.
Download the Latest tagged CVOYA Graph source asset (.zip)
The stable URL above follows whichever tagged GitHub Release is marked Latest. For an immutable download, use the asset on a specific release; install supported binaries from NuGet, or browse the repository when you want the current development branch.
CVOYA Graph is a type-safe .NET graph library with one provider-neutral model for LINQ queries, transactions, set-based mutations, relationship traversal, serialization, and provider certification. The repository includes Neo4j, PostgreSQL + Apache AGE, and in-memory providers.
IEntityis an identity-free marker.INodeadds runtimeLabels;IRelationshipadds runtimeType.NodeandRelationshipare convenient base records. They do not add IDs, endpoints, or direction.- Domain keys are optional and explicit with
[Property(IsKey = true)]. Multiple key properties form one composite key; keyless models are valid. - Properties named
IdorDirectionare ordinary user properties unless you explicitly annotate them. - Providers keep physical node/edge identity private and use it only to correlate database work.
- Relationships are connected through endpoint intent passed to the create API, not through relationship-owned endpoint IDs.
- Queries use
IGraphQueryable<T>and async terminals. Updates and deletes operate over a frozen, distinct query selection.
using Cvoya.Graph;
[Node(Label = "Person")]
public record Person : Node
{
[Property(IsKey = true)]
public string Tenant { get; init; } = string.Empty;
[Property(IsKey = true)]
public string Email { get; init; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public List<string?> Aliases { get; set; } = [];
public List<Address?> PreviousAddresses { get; set; } = [];
}
public record Address
{
public string City { get; init; } = string.Empty;
}
[Node(Label = "AuditEntry")]
public record AuditEntry : Node
{
public string Message { get; set; } = string.Empty;
}
[Relationship(Label = "KNOWS")]
public record Knows : Relationship
{
public DateTime Since { get; init; }
}Remove both IsKey declarations and Person becomes keyless. A key is domain schema metadata,
not provider identity or an implicit mutation target.
The inventory below covers every packable project under src/. Package validation derives the
expected set from those projects with scripts/verify-package-set.sh
instead of maintaining a second fixed inventory.
| Package | Purpose |
|---|---|
Cvoya.Graph |
Core model, query, transaction, mutation, and provider contracts |
Cvoya.Graph.Neo4j |
Neo4j provider |
Cvoya.Graph.Age |
PostgreSQL + Apache AGE provider |
Cvoya.Graph.InMemory |
In-process reference provider and test double |
Cvoya.Graph.Cypher |
Shared typed Cypher AST, planning, validation, and rendering SPI |
Cvoya.Graph.Serialization |
Provider-neutral serialization and result materialization |
Cvoya.Graph.Serialization.CodeGen |
Incremental entity serializer generator |
Cvoya.Graph.Analyzers |
Optional compile-time model diagnostics |
Cvoya.Graph.CompatibilityTests |
Provider compatibility suite (TCK) |
Install the provider for your database; it brings the required core and serialization packages:
dotnet add package Cvoya.Graph.Neo4j
dotnet add package Cvoya.Graph.Analyzersusing Cvoya.Graph;
using Cvoya.Graph.Neo4j;
await using var store = new Neo4jGraphStore(
"bolt://localhost:7687",
"neo4j",
"password",
databaseName: "myapp");
var graph = store.Graph;
var alice = new Person
{
Tenant = "northwest",
Email = "alice@example.com",
Name = "Alice",
Age = 30,
};
var bob = new Person
{
Tenant = "northwest",
Email = "bob@example.com",
Name = "Bob",
Age = 28,
};
await graph.CreateAsync(
alice,
new Knows { Since = DateTime.UtcNow },
bob);
var aliceSelection = graph.Nodes<Person>()
.Where(person => person.Tenant == "northwest" &&
person.Email == "alice@example.com");
await aliceSelection.UpdateAsync(setters => setters
.SetProperty(person => person.Age, person => person.Age + 1)
.SetProperty(person => person.Name, "Alice Smith"));
var connections = await aliceSelection
.PathSegments<Person, Knows, Person>()
.ToListAsync();
foreach (var segment in connections)
{
Console.WriteLine(
$"{segment.StartNode.Name} --{segment.Relationship.Type}--> " +
$"{segment.EndNode.Name} ({segment.Direction})");
}IGraphPathSegment.Direction reports the physical edge orientation relative to the returned
StartNode and EndNode. The Knows object itself has no endpoint or direction fields.
The API supports every selected/new endpoint combination:
var alice = graph.Nodes<Person>()
.Where(person => person.Email == "alice@example.com");
var bob = graph.Nodes<Person>()
.Where(person => person.Email == "bob@example.com");
// Two existing endpoints. Each query must select exactly one node.
await graph.CreateRelationshipAsync(alice, new Knows(), bob);
// Existing source and new target.
await graph.CreateAsync(alice, new Knows(), new Person
{
Tenant = "northwest",
Email = "charlie@example.com",
Name = "Charlie",
});
// New source and existing target.
await graph.CreateAsync(new Person
{
Tenant = "northwest",
Email = "dana@example.com",
Name = "Dana",
}, new Knows(), bob);
// One new keyless or keyed node connected to itself.
await graph.CreateSelfLoopAsync(
new AuditEntry { Message = "Keyless self-loop" },
new Knows());IGraph.CreateAsync(source, relationship, target) creates two new endpoints and the edge
atomically. Selected endpoints use exact-one semantics: zero or multiple rows fail instead of
guessing an endpoint.
var adults = graph.Nodes<Person>().Where(person => person.Age >= 18);
var updated = await adults.UpdateAsync(setters => setters
.SetProperty(person => person.Name, person => person.Name)
.SetProperty(person => person.PreviousAddresses, new List<Address?>()));
var deleted = await graph.Nodes<Person>()
.Where(person => person.Email.EndsWith("@expired.example"))
.DeleteAsync(cascadeDelete: true);Updates accept typed scalar, constrained, collection, and complex-property selectors. Providers
validate key/unique/required constraints and replace complex-property subtrees atomically.
Relationship queries have their own DeleteAsync() overload. There is no whole-entity update API.
Neo4j and AGE store ordinary typed labels, relationship types, and user properties in native graph storage. Typed, dynamic, traversal, and full-text queries can read compatible rows created by a native database client without CVOYA metadata. Reads do not provision labels, types, indexes, functions, or graphs; write paths provision only what they require.
| Surface | Neo4j | Apache AGE | In-memory |
|---|---|---|---|
| Full-text contract floor | Managed native full-text indexes | PostgreSQL text search over native AGE rows | Index-free whole-token scan |
| Relationship predicates | Yes | Yes, provider-local lowering | Yes |
Union / Concat |
Yes | Yes, provider-local lowering | Yes |
| Shortest path | Yes | No in v1.0; capability-gated TCK cases skip | Yes |
| Whole-entity ordering | Yes | No | No |
| Managed index recreation | Owned range/full-text indexes only | Successful no-op | Successful no-op |
All three providers preserve nullable simple and complex collection elements, exact order, and null positions. Physical companion properties used by database providers are private implementation details and never appear in typed or dynamic property bags.
The v1 model is deliberately incompatible with alpha-era universal IDs, relationship endpoint IDs, relationship-owned direction, universal root labels/types, and legacy provider metadata.
There is no compatibility reader, mixed legacy/native mode, dual write, automatic backfill, or supported migration script. Back up existing data, update the application model, then recreate and reimport into the final native representation or perform your own reviewed transformation. CVOYA Graph will not delete or rewrite an old database automatically. See the v1 migration guide.
- Core concepts
- LINQ querying
- Transactions
- Attributes and configuration
- Runtime metadata
- Testing with the in-memory provider
- Provider implementers guide
- Neo4j provider
- Apache AGE provider
- Migration from 0.x
- API reference
Compiling examples live under examples/.
Requires the .NET 10 SDK and C# 14. Provider-backed tests also require their corresponding service.
dotnet build --configuration Debug
# Service-free projects plus the in-memory provider contract suite
./scripts/run-tests.sh --configuration Debug --lane fast --disable-diff-engine
# Start each provider service and run the full discovered suite
./scripts/run-tests.sh --configuration Debug --lane all --neo4j --age --disable-diff-engine
# Validate the derived package set and package-reference build
dotnet msbuild eng/PackageValidation.proj -target:ValidateSee CONTRIBUTING.md, tests/README.md, and the build-system guide for the lane and service details.
CVOYA Graph thanks Paul Jeschke (@paule96) for contributing the
original PostgreSQL + Apache AGE provider in
pull request #66. His work helped shape the
provider-neutral architecture and the AGE provider included today.
Licensed under the Apache License, Version 2.0. See LICENSE.