Nodal.Migrations 0.1.0-alpha.4

This is a prerelease version of Nodal.Migrations.
dotnet add package Nodal.Migrations --version 0.1.0-alpha.4
                    
NuGet\Install-Package Nodal.Migrations -Version 0.1.0-alpha.4
                    
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="Nodal.Migrations" Version="0.1.0-alpha.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nodal.Migrations" Version="0.1.0-alpha.4" />
                    
Directory.Packages.props
<PackageReference Include="Nodal.Migrations" />
                    
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 Nodal.Migrations --version 0.1.0-alpha.4
                    
#r "nuget: Nodal.Migrations, 0.1.0-alpha.4"
                    
#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 Nodal.Migrations@0.1.0-alpha.4
                    
#: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=Nodal.Migrations&version=0.1.0-alpha.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Nodal.Migrations&version=0.1.0-alpha.4&prerelease
                    
Install as a Cake Tool

Nodal Framework

Nodal Framework is a provider-based .NET graph data access prototype. It keeps the domain model and query API provider-neutral while Neo4j and TigerGraph packages compile and execute the same model through their native transports.

Packages

Package Purpose
Nodal.Core Provider-neutral model, LINQ query surface, tracking, and unit of work
Nodal.Migrations Portable graph schema migration contracts and planning
Nodal.Neo4j Neo4j/Cypher provider using the official pooled Bolt driver
Nodal.PatternRecognition Experimental provider-neutral analytics shell for path similarity and pattern discovery
Nodal.TigerGraph TigerGraph/GSQL provider using REST++ and an optional administrative transport

The initial alpha targets .NET 10. Package versions move together so provider and core contracts remain compatible during the pre-release period.

Install one provider package; it brings Nodal.Core transitively. Add the migration package only when the application owns schema evolution:

dotnet add package Nodal.Neo4j --prerelease
# or: dotnet add package Nodal.TigerGraph --prerelease
dotnet add package Nodal.Migrations --prerelease

Nodal.PatternRecognition is the optional P3 analytics shell above providers. Its first executable alpha slice includes exact allocation-free bitset similarity; community, sequence, and temporal-transition surfaces remain experimental roadmap work.

Pin all packages to the same version for reproducible builds, for example 0.1.0-alpha.2. The complete console, worker, and ASP.NET Core setup is in the installation guide.

Compatibility and provider capabilities

Nodal distinguishes vendor client compatibility from versions verified by this repository. The current live QA baselines are Neo4j 5.26 Community and TigerGraph 4.2.4 Community. Nodal.Neo4j uses Neo4j.Driver 6.3.0; the vendor states that driver 6.x connects to Neo4j 4.4.x, 5.x, 2025.x, and 2026.x, but those additional server families are not yet Nodal-certified. Neo4j 5.26 analytics require the vendor-matched GDS 2.13 release.

Capability Neo4j TigerGraph
Parameterized queries and fixed traversals Supported Supported
Variable-depth traversal Supported GSQL Syntax V2 with documented restrictions
Optional match Supported Not supported
Transaction boundary Client-managed transaction Atomic request or installed query
Migration execution Supported Requires administrative transport
Centrality and community detection Requires compatible GDS and named projection Requires explicitly configured installed GSQL query
Weighted analytics Algorithm-specific Must be declared for each installed query
Typed shortest paths Native Cypher; GDS for weighted algorithms Configured installed GSQL query

Analytics families currently represented by the portable contract:

Family Algorithms Neo4j status TigerGraph status
Centrality ArticleRank, articulation points, betweenness, bridges, CELF, closeness, degree, eigenvector, harmonic, HITS, PageRank GDS compiler; live certification pending Installed-query contract; configured per deployment
Community Clique counting, conductance, HDBSCAN, K-core, K-1 coloring, K-means, label propagation, Leiden, local clustering coefficient, Louvain, modularity, modularity optimization, SCC, triangle count, WCC, maximum k-cut, SLLPA GDS compiler; live certification pending Installed-query contract; configured per deployment
Path finding Shortest/all-shortest, Dijkstra, A*, Yen Native unweighted execution and GDS weighted compiler Installed-query execution with canonical routes

The full matrix and verification legend are published in the compatibility documentation. Unsupported capabilities fail before transport and are never emulated by downloading the graph into application memory.

Weekly Dependabot checks cover NuGet, documentation npm packages, Docker database images, and GitHub Actions. Update pull requests target developer; a database version becomes a supported Nodal baseline only after its compatibility and live integration suites pass.

Attribute-based model

Attributes describe only portable graph semantics. Database-specific indexes, constraints, storage options, and migration details remain in the fluent migration API so domain POCOs do not become coupled to one graph database.

using Nodal.Core.Metadata;

[GraphNode("Person")]
public sealed record Person(
    [property: GraphKey]
    [property: GraphProperty("person_id")]
    string Id,

    [property: GraphProperty("display_name")]
    string Name)
{
    [GraphIgnore]
    public string DisplayLabel => $"{Name} ({Id})";
}

[GraphRelation("KNOWS", Directed = true)]
public sealed class Knows(DateTime since)
{
    [GraphProperty("since_at")]
    public DateTime Since { get; set; } = since;
}
  • [GraphNode] maps a CLR node type to its provider-neutral label or vertex type.
  • [GraphRelation] maps an edge POCO and records whether its direction is meaningful.
  • [GraphKey] selects the stable domain identifier. By convention, Id and <TypeName>Id also work.
  • [GraphProperty] maps both node and relationship payload properties.
  • [GraphIgnore] excludes calculated, transient, or application-only properties.

Fluent model configuration has the highest precedence, attributes come next, and conventions are the fallback.

Context and strongly typed sets

Public GraphSet<T> and RelationSet<TSource, TRelation, TTarget> properties are discovered automatically:

public sealed class SocialGraphContext(IGraphProvider provider) : NodalContext(provider)
{
    public GraphSet<Person> People => Set<Person>();

    public RelationSet<Person, Knows, Person> Friendships =>
        Relations<Person, Knows, Person>();
}

The same LINQ expression is translated using the mapped graph property names:

var adults = await context.People
    .Match(person => person.Name == "Ada" && person.Id == "person-42")
    .Take(10)
    .ToListAsync();

Relationships can be traversed without introducing provider-specific query text. Where after a traversal filters the reached node, and the result is materialized as that node's POCO type:

var peopleKnownByAda = await context.People
    .Match(person => person.Id == "person-42")
    .Traverse(context.Friendships)
    .Where(person => person.Name != "Grace")
    .Take(10)
    .ToListAsync();

var peopleWhoKnowAda = await context.People
    .Match(person => person.Id == "person-42")
    .TraverseIncoming(context.Friendships)
    .ToListAsync();

The same provider-neutral traversal model compiles to directed Cypher patterns for Neo4j and directed GSQL path patterns for TigerGraph. Relations declared with Directed = false automatically use an undirected traversal.

Query engine

The fluent query surface keeps values parameterized while pushing filtering, ordering, paging, distinctness, traversal, and aggregates into the selected provider:

string[] selectedIds = ["person-42", "person-84"];

var page = await context.People.Query()
    .Where(person => selectedIds.Contains(person.Id))
    .Where(person => person.Name.StartsWith("Ad") && person.Name != null)
    .OrderBy(person => person.Name)
    .ThenByDescending(person => person.Id)
    .Skip(20)
    .Take(10)
    .Distinct()
    .AsNoTracking()
    .Select(person => new { person.Id, person.Name })
    .ToListAsync();

var exists = await context.People.Match(person => person.Name.Contains("Lovelace")).AnyAsync();
var count = await context.People.Match(person => selectedIds.Contains(person.Id)).CountAsync();
var person = await context.People.Match(person => person.Id == "person-42").SingleAsync();

FirstAsync, FirstOrDefaultAsync, SingleAsync, SingleOrDefaultAsync, AnyAsync, and CountAsync apply bounded or aggregate execution. CountAsync uses a server-side aggregate when paging has not changed LINQ count semantics. AsAsyncEnumerable provides cancellation-aware asynchronous consumption; HTTP providers necessarily receive one response payload, while the API keeps consumer code provider-neutral.

Graph analytics retain the same typed model while executing centrality and community algorithms on the provider:

var influentialPeople = await context.People.Query()
    .Analyze(context.Friendships)
    .PageRank()
    .OnProjection("social")
    .Top(20)
    .ToListAsync();

var communities = await context.People.Query()
    .Analyze(context.Friendships)
    .Louvain(new LouvainOptions(MaximumLevels: 8))
    .OnProjection("social")
    .ToListAsync();

The analytics contract covers the full centrality and community-detection families and preserves algorithm-specific metrics for HITS, bridges, components, cliques, clustering, and modularity. Neo4j uses explicitly enabled GDS procedures; TigerGraph advertises only explicitly configured installed GSQL query endpoints. Unsupported operations fail before transport execution and are never emulated by downloading the graph into application memory.

Shortest paths keep both endpoints strongly typed:

GraphRoute<Person, Knows> route = await context.People
    .Match(person => person.Id == sourceId)
    .ShortestPathTo(context.People.Match(person => person.Id == targetId), context.Friendships)
    .MaxDepth(8)
    .SingleAsync();

Neo4j GDS deployments expose discovery and projection create/reuse/drop operations through context.Database.GetAnalyticsRuntime(). TigerGraph exposes its configured installed-query snapshot through the same segregated runtime contract.

Hot query factories can be compiled once:

var personById = NodalCompiledQuery.Compile((SocialGraphContext database, string id) =>
    database.People.Match(person => person.Id == id));

var ada = await personById(context, "person-42").SingleAsync();

Graph-native queries support incoming, outgoing, and undirected hops, repeated-hop depth bounds, multiple compatible edge types, explicit cycle policy, and normalized subgraph output:

GraphQueryResult neighborhood = await context.People
    .Match(person => person.Id == "person-42")
    .Traverse(context.Friendships, minDepth: 1, maxDepth: 3)
    .WithoutCycles()
    .ToSubgraphAsync();

Neo4j compiles repeated hops and simple paths to Cypher. TigerGraph switches only repeated-hop queries to GSQL Syntax V2 and keeps fixed traversals on the stable Syntax V1 path. Unsupported semantic combinations fail explicitly: TigerGraph does not emulate optional match or a vertex-simple variable-depth path when GSQL cannot expose the required intermediate aliases.

Provider-native escape hatches remain parameterized and return the same normalized result contracts:

var rawPeople = await context.Database.QueryRawAsync<Person>(
    "MATCH (`node`:`Person`) WHERE `node`.`person_id` = $id RETURN `node`",
    new Dictionary<string, object?> { ["id"] = "person-42" });

Use a path projection when the relationship payload is part of the domain operation:

var path = await context.People
    .Match(person => person.Id == "person-42")
    .TraversePath(context.Friendships)
    .WhereRelation(knows => knows.Since >= new DateTime(2024, 1, 1))
    .WhereTarget(person => person.Name != "Grace")
    .SingleAsync();

Console.WriteLine(path.Source);
Console.WriteLine(path.Relation);
Console.WriteLine(path.Target);

Path nodes and relationships participate in the context identity map. Repeating the query therefore reuses tracked instances. A mutable relationship payload can be persisted through the same unit of work:

path.Relation.Since = DateTime.UtcNow;
context.Friendships.Update(path.Source, path.Relation, path.Target);
await context.SaveChangesAsync();

ToRelationsAsync() is available when only relationship payloads are needed. Neo4j updates a queried relationship by its exact provider element identity, preserving parallel edges. TigerGraph emits source, edge, and target output through typed GSQL accumulators and normalizes the response to the same canonical path records.

Provider construction

Neo4j uses one long-lived official driver instance and therefore reuses its Bolt connection pool:

await using var provider = new Neo4jProvider(new Neo4jOptions
{
    Endpoint = new Uri("neo4j://localhost:7687"),
    Username = "neo4j",
    Password = "secret",
    Database = "neo4j"
});

var context = new SocialGraphContext(provider);

TigerGraph uses an externally managed HttpClient, allowing applications to control HTTP pooling and handler lifetime:

var httpClient = new HttpClient();
var provider = new TigerGraphProvider(
    httpClient,
    new TigerGraphOptions
    {
        Endpoint = new Uri("https://example.i.tgcloud.io/"),
        Username = "tigergraph",
        Password = "secret"
    },
    graphName: "SocialGraph");

var context = new SocialGraphContext(provider);

Both providers produce Nodal's canonical result model before POCO materialization. Provider-specific response shapes therefore do not escape into application code.

Unit of work

Node and relationship mutations are collected in the context and converted into an ordered, provider-neutral mutation plan:

var ada = new Person("person-1", "Ada");
var alan = new Person("person-2", "Alan");

context.People.Add(ada);
context.People.Add(alan);
context.Friendships.Connect(ada, new Knows(DateTime.UtcNow), alan);

GraphSaveResult result = await context.SaveChangesAsync();

New nodes are planned before their relationships. Relationship deletions are planned before node deletions. Entry states are accepted only after the provider confirms a successful commit; a failed commit therefore remains retryable. Providers opt into writes through IGraphMutationProvider, allowing read-only providers to remain valid.

Neo4j executes the full plan inside a client-managed write transaction. TigerGraph executes vertex and edge upserts as one REST transaction with gsql-atomic-level: atomic; its capability metadata therefore reports RequestOrQuery transaction scope. When a plan contains a deletion, Nodal derives a deterministic query name from the operation shape, creates and installs one parameterized nodal_apply_mutations_* GSQL query through the configured administrative transport, and invokes its REST endpoint once. Later plans with the same shape reuse that installed query. A definition or installation failure occurs before the data endpoint is called, and Nodal never splits one unit of work into non-atomic requests.

Pending work can be inspected without exposing provider-specific commands:

var pending = context.ChangeTracker.Entries(GraphEntryState.Added);

Queries use identity resolution by default. Tracked mutable POCOs are compared with their original mapped-property snapshots when SaveChangesAsync runs, so calling Update is not required for ordinary property edits. AutoDetectChangesEnabled, DetectChanges, Entry, Attach, Detach, property-level IsModified, AsNoTracking, and ReloadAsync provide explicit control for high-volume workloads.

Migrations

Migrations declare portable schema intent and carry a stable history identifier:

public sealed class InitialSocialGraph : NodalMigration
{
    public override string Id => "20260816_001_initial_social_graph";

    protected override void Up(MigrationBuilder migration) => migration
        .CreateNode<Person>()
        .CreateRelation<Knows, Person, Person>()
        .CreateIndex<Person, string>(person => person.Name);

    protected override void Down(MigrationBuilder migration) => migration
        .DropRelation<Knows>()
        .DropNode<Person>();
}

Planning is side-effect free. Execution skips identifiers already stored in provider history:

NodalMigration[] migrations = [new InitialSocialGraph()];

MigrationPlan dryRun = await context.Database.PlanMigrationsAsync(migrations);
MigrationPlan applied = await context.Database.MigrateAsync(migrations);

Plans contain deterministic SHA-256 checksums and provider-specific commands. Neo4j applies its commands and __NodalMigration history record in one write transaction. TigerGraph compiles typed vertex, edge, and secondary-index operations into one deterministic schema-change job. TigerGraph administrative execution remains an explicit provider capability because its supported REST API exposes schema inspection and query installation but not a general arbitrary-DDL endpoint; the framework does not silently invent or depend on an undocumented route.

TigerGraph migration execution is enabled only when the host supplies an administrative transport appropriate to its deployment. Self-managed and local Docker installations can use the included documented GSQL process transport:

ITigerGraphAdministrativeTransport administration = new TigerGraphGsqlProcessTransport(
    new TigerGraphGsqlProcessOptions
    {
        FileName = "docker",
        PrefixArguments = ["exec", "nodal-tigergraph", "gsql"],
        GraphName = "SocialGraph"
    });
var provider = new TigerGraphProvider(
    httpClient,
    tigerGraphOptions,
    "SocialGraph",
    administration);

The migration executor bootstraps the __NodalMigration vertex type when necessary, records checksums through an atomic REST++ upsert, and removes temporary schema jobs even when job execution fails. The same administrative channel enables lazy installation of transactional mutation queries required by delete-containing units of work. Without it, querying and atomic create/update batches remain available while migrations and delete plans report an explicit unsupported-capability error. Because mutation dictionaries currently carry runtime values rather than declared property metadata, a null property in a delete-containing compiled plan is rejected instead of guessing an unsafe GSQL parameter type.

Documentation

The documentation platform combines an English Docusaurus guide and journal with a DocFX API reference generated from the product's XML documentation. Machine-readable llms.txt, extended coding-agent context, and a JSON-LD capability graph are published with the static site.

Build the complete site locally with:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/build-docs.ps1

The production output is written to website/build. During authoring, restore the website packages and start the live Docusaurus server with:

npm.cmd ci --prefix website
npm.cmd run start --prefix website

API pages under /api are generated by DocFX before the Docusaurus build. See docs/DEPLOYMENT.md for the Cloudflare Pages setup and required GitHub environment secrets.

Quality gate

The repository has one local command matching the CI quality job:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/verify.ps1

It restores dependencies, verifies formatting, builds in Release mode, runs the complete test suite, enforces the Core package's minimum 95% line-coverage gate, and validates the publishable NuGet archives. Coverage can also be run independently:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/verify-core-coverage.ps1

The script rebuilds the Core tests, produces a Cobertura report under the ignored TestResults directory, and fails when line coverage falls below the threshold.

Package verification can also be run independently:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/verify-packages.ps1

The package gate produces all five .nupkg and .snupkg artifacts, then inspects their manifests and contents for the MIT expression, repository metadata, README, license, IntelliSense XML, target framework, and required package dependencies.

Publishing

Alpha packages are published only after a pull request promotes developer to staging. The Publish Alpha Packages workflow assigns one immutable 0.1.0-alpha.<run> version to all five packages, runs the complete QA gate, exchanges GitHub's OIDC identity for a short-lived NuGet credential, and publishes Nodal.Core before its dependent packages. No long-lived NuGet API key is stored by the repository.

The GitHub staging environment must define NUGET_USER as the NuGet profile name. NuGet Trusted Publishing must match repository owner Greenstone-Research-Lab, repository NodalFramework, workflow file publish-alpha.yml, and environment staging. Publishing deliberately does not use --skip-duplicate, ensuring package conflicts and reserved identifiers fail visibly.

Live integration tests

Live database tests are isolated in Nodal.IntegrationTests and are skipped during ordinary unit-test runs unless their environment is configured.

Local Docker stack

Neo4j and TigerGraph Community can be started together as a persistent local development stack:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/start-local-databases.ps1

Neo4j Browser is available at http://localhost:7474 with local-only credentials neo4j / NodalLocal123!. TigerGraph GraphStudio and its consolidated REST/GSQL endpoint are available at http://localhost:14240 with the Community image's local credentials tigergraph / tigergraph; the startup script creates the NodalQa graph with the Person vertex and KNOWS edge schema used by the integration suite. TigerGraph is substantially larger than Neo4j and requires at least 8 GB of Docker memory.

Run both live provider suites against the persistent containers with:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/run-local-integration.ps1

Stop containers while preserving their data volumes with:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/stop-local-databases.ps1

The compose ports and credentials are intentionally limited to loopback-bound local development. REST++ data authentication is disabled by the Community image's local configuration, while interpreted GSQL requests use Basic authentication. These settings must not be reused for shared or production deployments.

Runnable provider demos

The samples directory contains one shared social graph model and two console hosts. Both hosts execute the same provider-neutral create, path traversal, update, and verification workflow; only provider construction differs.

With the local Docker stack available, run both demos using:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/run-local-demos.ps1

The generated Ada -> KNOWS -> Alan paths remain in each database for visual inspection. Connection settings can be overridden through the documented NODAL_NEO4J_* and NODAL_TIGERGRAPH_* environment variables.

Neo4j can be started in a disposable Docker container and tested end to end with:

powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/run-neo4j-integration.ps1

The runner publishes a temporary Bolt port, supplies credentials only through process environment variables, runs commit/read/delete and rollback tests, and removes its uniquely named container in a finally block.

TigerGraph live tests expect a dedicated QA graph containing Person vertices and KNOWS edges with the properties used by the sample model:

$env:NODAL_TIGERGRAPH_ENDPOINT = 'https://your-host/'
$env:NODAL_TIGERGRAPH_ACCESS_TOKEN = 'secret-token'
$env:NODAL_TIGERGRAPH_GRAPH = 'NodalQa'
powershell -NoProfile -ExecutionPolicy Bypass -File ./eng/run-tigergraph-integration.ps1

No live credentials are stored in the repository. The integration project intentionally avoids the current Testcontainers dependency chain until its high-severity SSH.NET advisory is available through a patched NuGet release.

GitHub Actions runs the quality gate and a disposable Neo4j smoke environment for every pull request targeting developer, staging, or master. TigerGraph smoke tests run only outside pull requests when the repository variable NODAL_RUN_TIGERGRAPH is true; credentials are read from the protected tigergraph-qa environment secrets NODAL_TIGERGRAPH_ENDPOINT, NODAL_TIGERGRAPH_ACCESS_TOKEN, and NODAL_TIGERGRAPH_GRAPH. The TigerGraph suite verifies create/read/update persistence and confirms that an invalid edge rolls back vertices in an atomic REST batch.

License

Nodal Framework is distributed under the MIT License.

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
0.1.0-alpha.4 0 8/21/2026
0.1.0-alpha.3 0 8/20/2026
0.1.0-alpha.2 0 8/20/2026
0.1.0-alpha.1 54 8/16/2026