Cvoya.Graph.Age 1.0.0

Prefix Reserved
dotnet add package Cvoya.Graph.Age --version 1.0.0
                    
NuGet\Install-Package Cvoya.Graph.Age -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cvoya.Graph.Age" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cvoya.Graph.Age" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Cvoya.Graph.Age" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Cvoya.Graph.Age --version 1.0.0
                    
#r "nuget: Cvoya.Graph.Age, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Cvoya.Graph.Age@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Cvoya.Graph.Age&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Cvoya.Graph.Age&version=1.0.0
                    
Install as a Cake Tool

Downloadable open-source computer software from CVOYA. See the CVOYA software catalog.

Cvoya.Graph.Age

Cvoya.Graph.Age is the PostgreSQL + Apache AGE provider for CVOYA graph. It uses the shared Cvoya.Graph.Cypher dialect SPI and exposes the same typed graph, LINQ, serialization, and transaction APIs as the other providers.

Install

dotnet add package Cvoya.Graph.Age

The PostgreSQL database must have the Apache AGE extension installed. The provider loads AGE and sets the required ag_catalog search path on every pooled connection.

Connect

using Cvoya.Graph;
using Cvoya.Graph.Age;

await using var store = new AgeGraphStore(
    "Host=localhost;Port=5455;Username=postgres;Password=postgres;Database=postgres",
    graphName: "my_graph");

await store.CreateGraphIfNotExistsAsync();
var graph = store.Graph;

var activeUsers = await graph.Nodes<User>()
    .Where(user => user.IsActive)
    .ToListAsync();

Production applications should supply the connection string directly or through AGE_CONNECTION_STRING; the provider deliberately has no fallback credentials. AGE_GRAPH selects the default graph name when the constructor does not specify one.

An application that already owns its connection pool can pass an AGE-enabled NpgsqlDataSource. In that form, disposing the store does not dispose the caller's data source.

Supported surface

  • typed and dynamic node/relationship CRUD;
  • shared LINQ-to-Cypher translation and streaming queries;
  • ACID transactions on a dedicated PostgreSQL connection;
  • atomic endpoint–relationship–endpoint creation in one Npgsql batch round-trip, including nested complex-property subtrees and create-missing endpoint semantics;
  • complex-property persistence and cascading cleanup;
  • native logical AGE labels/types for model roots and catalog-vetted raw native data;
  • write-scoped native label/type provisioning (ordinary reads perform no DDL);
  • scalar-key grouped aggregation through AGE-native WITH grouping and aggregate functions;
  • correlated collection projections and relationship/complex-collection counts, lowered to AGE-supported grouped matches (see below); and
  • agtype adaptation for vertices, edges, paths, maps, arrays, large integers, decimals, and ISO-8601 temporal values.

The provider declares full-text search, relationship predicates, and set operations. It does not declare nested transactions, whole-entity ordering, or GraphCapability.ShortestPath. Shortest path is deliberately unsupported for v1.0; the capability-gated TCK test skips for AGE, and #355 tracks any future implementation. Unsupported operations fail during translation or are skipped only when a compatibility test is explicitly gated by the missing capability.

Native storage and commands

New Person nodes are stored in AGE's Person label table and new KNOWS relationships use the native KNOWS type. Concrete raw AGE rows need no CVOYA discriminator, inheritance, entity-kind, or CLR metadata to participate in typed, dynamic, and traversal queries. Provider-owned complex-value nodes remain isolated through their relationship marker and reserved physical storage.

AGE catalog label names are limited to plain symbolic names. A mapped name outside that grammar (for example, a label containing a space) uses the provider-reserved physical-table representation so the escaped-identifier API remains usable; reserved provider names are rejected before writing.

Set updates and deletes first freeze distinct id(n) / id(r) values inside the active write transaction, then mutate only those graphids. Endpoint-intent relationship creation likewise joins selected or newly created endpoints by graphid and supports selected/selected, hybrid, all-new, and explicit self-loop shapes. Caller-owned multi-statement commands are isolated behind a savepoint. The public set-based mutation and endpoint-intent APIs use these native command paths without exposing graphids.

Scalar-key grouped aggregation

The shared planner represents scalar-key GroupBy as a structured WITH stage: the non-aggregate key establishes AGE's implicit grouping key, while Count, Sum, Average, Min, and Max become aggregate columns consumed by the final projection. Key-only projections use WITH DISTINCT. The existing AGE AST passes then lower labels, reserved aliases, and empty sums without inspecting or rewriting rendered Cypher. The supported and rejected shapes therefore match the provider-neutral GroupByAggregation contract; unsupported entity keys, element selectors, collection projections, and filters inside a group fail before database execution with the shared actionable diagnostic.

Correlated collections and pattern counts

Apache AGE 1.7 cannot execute Cypher pattern comprehensions or CALL {} subqueries, and it parses EXISTS { ... } / COUNT { ... } pattern subqueries but silently matches nothing. The provider implements the corresponding shared capabilities through an AGE-local structured AST pass before rendering. Correlated collection queries match the traversal once and group by its source node, using collect, conditional count, ordinary aggregates, and ordered/nested WITH stages as required; a filter inside one projection (group.Where(...).Average(...)) becomes conditional aggregation so it cannot narrow the rows sibling projections aggregate over, and a segment filter becomes a grouped row-count guard so owners with no qualifying rows produce no result row. Independent relationship-degree and complex-collection counts — in projections, orderings, and Where filters (including .Any(...) existence checks) — use sequential OPTIONAL MATCH stages so owners with no match return zero and multiple counts in one projection do not multiply each other. Two correlated shapes have no equivalent staging and fail at translation time instead: a filtered nested grouping and multiple ordered collections in one projection. The resulting nested lists and maps continue through the provider-neutral result wire model and shared materializer.

AGE full-text search discovers the graph's concrete vertex/edge tables from the AGE catalog, searches native logical storage and externally managed labels, then correlates the combined distinct result through transaction-local id(n) / id(r) values. The provider-reserved CvoyaNode and CvoyaRelationship tables are excluded from external root discovery. Graphids remain provider plumbing and are never substituted for public Id data; a domain property named Id participates like any other included string property.

Registered labels search exactly their included string properties, and a registered type with no included property matches nothing. Global searches use the all-string fallback only for genuinely unregistered external labels; dynamic entities retain their public all-string-value contract. Ordinary reads inline the PostgreSQL text-search predicate and require no managed function, index, or DDL permission. The final native AGE provider owns no index artifacts, so RecreateManagedIndexesAsync is a successful no-op: it performs no discovery, function creation, index creation, drop, rebuild, or graph provisioning. Existing PostgreSQL artifacts are untouched.

Local AGE

From the repository root:

scripts/containers/start-age.sh
export AGE_CONNECTION_STRING='Host=localhost;Port=5455;Username=postgres;Password=postgres;Database=postgres'
dotnet test tests/Graph.Age.Tests/Graph.Age.Tests.csproj --configuration Debug

The implementation incorporates and substantially reworks Apache AGE provider ideas from @paule96's PR #66. In particular, the connection bootstrap, agtype conversion, SQL envelope, transaction, serialization, and CRUD designs informed this provider; the dialect-SPI implementation and security hardening were performed as part of issue #86.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 43 7/23/2026
1.0.0-alpha.20260717 50 7/17/2026