Neo4jLiteRepo is a .NET library designed to simplify Neo4j database interactions by providing a clear, attribute-driven pattern for modeling, seeding, and querying graph data. It enables rapid onboarding for .NET developers and supports both local and AuraDB Neo4j instances.
- Neo4jLiteRepo: Core library with all essential functionality (targets .NET 10)
- Neo4jLiteRepo.Importer: Sample project demonstrating data import and configuration
- Neo4jLiteRepo.Sample: Example node models, edge models, and node services
- Neo4jLiteRepo.Tests: Unit tests for the library
- Neo4jLiteRepo.IntegrationTests: Integration tests (CRUD, relationships, schema, maintenance, seeding)
- Node.Training: Code generator that produces node classes and services from sample JSON data
- All node models inherit from
GraphNode. - Mark one property with
[NodePrimaryKey](must be unique). - Use
[NodeProperty]for properties to be stored in Neo4j. - Use
[NodeRelationship<T>]for relationship properties (must beIEnumerable<string>containing primary key values of related nodes). - Implement the abstract
BuildDisplayName()andGetMainContent()methods for each node. - Features:
- Improved primary key handling:
GraphNodeenforces that only one property is decorated with[NodePrimaryKey]and throws clear exceptions if not found or misused. GetPrimaryKeyName()andGetPrimaryKeyValue()for robust access to primary key info.- Static helpers:
GraphNode.GetPrimaryKeyName<T>()andGraphNode.GetLabelName<T>(). LabelNameandNodeDisplayNamePropertyare virtual and use helper extensions for casing.Upsertedproperty tracks last upsert time (auto-set).EnforceUniqueConstraintvirtual property (defaults totrue).
- Improved primary key handling:
Example:
public class Movie : GraphNode
{
[NodePrimaryKey]
public string Title { get; set; }
[NodeProperty(nameof(Released))]
public int Released { get; set; }
[NodeRelationship<Genre>("IN_GENRE")]
public IEnumerable<string> Genres { get; set; }
public override string BuildDisplayName() => Title;
public override string GetMainContent() => $"{Title} ({Released})";
}- Each node type requires a corresponding node service implementing
INodeService(usually inherit fromFileNodeService<T>). - Responsible for loading data, providing type info, and configuring unique constraints.
- Register all node services in your DI container:
builder.Services.AddSingleton<INodeService, MovieNodeService>();- Define relationships using
[NodeRelationship<T>]asIEnumerable<string>properties. - Relationship names should be descriptive and UPPERCASE_WITH_UNDERSCORES (e.g.,
HAS_GENRE). - Both sides of a relationship should be defined for bidirectionality if needed.
Neo4jGenericRepois the main entry point for database operations (node/relationship upsert, Cypher queries, constraints).- API Surface:
- CRUD Operations:
LoadAsync<T>,LoadAllAsync<T>(with pagination support),DetachDeleteAsync<T>,DetachDeleteManyAsync<T>, andDetachDeleteNodesByIdsAsyncfor managing node lifecycle. - Flexible API Architecture: Most operations now support three usage patterns:
- Standalone (creates own session/transaction) - simplest for single operations
- Session-based (caller manages session) - efficient for batching multiple operations
- Transaction-based (caller manages transaction) - full control for complex atomic operations
- Relationship Management:
MergeRelationshipAsync,DeleteRelationshipAsync,DeleteEdgesAsync, andDeleteRelationshipsOfTypeFromAsyncwith session/transaction overloads for fine-grained control. - Maintenance Operations:
RemoveOrphansAsync<T>removes nodes with no relationships using efficient batch deletion. - Advanced Querying:
ExecuteReadListAsync<T>,ExecuteReadScalarAsync<T>,ExecuteReadStreamAsync<T>for streaming large result sets, and vector similarity search methods. - Schema Management:
EnforceUniqueConstraintsForAllGraphNodes(assembly-scanning),CheckMissingConstraintsAsync(dry-run),GetGraphMapAsJsonAsync, and vector index creation. - Edge Population:
PopulateEdgeObjectsAsync<T>hydrates edge object collections from relationship ID lists.LoadRelatedAsync<TSource, TRelated>combines path traversal with edge population. - Structured Vector Search:
ExecuteVectorSimilaritySearchStructuredAsyncreturns typedStructuredVectorSearchRowresults. - Improved error handling and diagnostics for all operations.
- CRUD Operations:
- Interface Decomposition: The repo implements focused interfaces (
INeo4jNodeCrudRepository,INeo4jReadRepository,INeo4jRelationshipRepository,INeo4jMaintenanceRepository,INeo4jSchemaRepository,INeo4jVectorSearchRepository,INeo4jSessionRepository) for clean dependency injection. - Performance Note:
- The repository passes parameters directly to Cypher queries for significant performance improvements.
- Use session-based overloads to batch multiple operations efficiently.
- Use transaction-based overloads when you need atomicity across multiple operations.
- Prefer repository methods over custom Cypher unless advanced queries are needed.
- The
CustomEdgeabstract class represents relationships with custom properties between nodes. - Key Features:
GetFromId()/GetToId()abstract methods identify the source and target nodes.- Use
[EdgePropertyIgnore]to exclude properties from edge serialization. - Pass the edge seed type as the second argument to
[NodeRelationship<T>]to associate an edge class with a relationship.
- Edge objects can be populated on-demand via
PopulateEdgeObjectsAsync<T>().
Example:
public class MovieGenreEdge : CustomEdge
{
public override string GetFromId() => MovieId;
public override string GetToId() => GenreId;
public string MovieId { get; set; }
public string GenreId { get; set; }
public string SampleEdgeProperty { get; set; }
}
// On the node, reference the edge type:
[NodeRelationship<Genre>("IN_GENRE", typeof(MovieGenreEdge))]
public IEnumerable<string> GenreIds { get; set; } = [];
public List<MovieGenreEdge>? InGenreEdges { get; set; }The repository provides comprehensive CRUD operations with flexible session/transaction management:
// Load single node
var movie = await repo.LoadAsync<Movie>("movie-id");
// Load all nodes with pagination
var movies = await repo.LoadAllAsync<Movie>(skip: 0, take: 50);
// Upsert single node (creates own session)
await repo.UpsertNode(movie);
// Upsert multiple nodes efficiently using shared session
await using var session = repo.StartSession();
await repo.UpsertNodes(movies, session);
// Delete single node (detaches all relationships)
await repo.DetachDeleteAsync<Movie>("movie-id");
// Delete multiple nodes by ID
await repo.DetachDeleteManyAsync<Movie>(new[] { "id1", "id2" });Create and manage relationships between nodes:
// Merge (create if not exists) a relationship
await repo.MergeRelationshipAsync(movie, "IN_GENRE", genre);
// Delete a specific relationship
await repo.DeleteRelationshipAsync(movie, "IN_GENRE", genre, EdgeDirection.Outgoing);
// Delete all relationships of a type from a node
await using var tx = await session.BeginTransactionAsync();
await repo.DeleteRelationshipsOfTypeFromAsync(movie, "IN_GENRE", EdgeDirection.Outgoing, tx);
await tx.CommitAsync();Keep your graph clean:
// Remove orphan nodes (nodes with no relationships)
var removedCount = await repo.RemoveOrphansAsync<Movie>();Load related nodes across relationship paths, optionally hydrating edge objects:
// Load related nodes via path traversal
var genres = await repo.LoadRelatedNodesAsync<Movie, Genre>(movieId, "IN_GENRE", hops: 1);
// Load related nodes with edge objects populated
var genres = await repo.LoadRelatedAsync<Movie, Genre>(movieId, "IN_GENRE", hops: 1, includeEdgeObjects: true);
// Populate edge objects on a node on-demand
await repo.PopulateEdgeObjectsAsync(movie);Enforce constraints and inspect the graph schema:
// Assembly-scanning constraint enforcement (discovers all GraphNode types)
await repo.EnforceUniqueConstraintsForAllGraphNodes();
// Dry-run: check which constraints are missing without creating them
var missing = await repo.CheckMissingConstraintsAsync();
// Get full graph schema as JSON
var schemaJson = await repo.GetGraphMapAsJsonAsync();- AuraDB: Get a free instance. Use the instance ID for
Neo4jSettings:Connectioninappsettings.json. - Local Docker:
$now = Get-Date -Format "yyyyMMdd"
$product = "neo4jlite"
docker run -d --rm `
--name neo4j-$product-$now `
-e server.memory.heap.initial_size=1G `
-e server.memory.heap.max_size=4G `
-e server.memory.pagecache.size=2G `
-v C:\Projects\yourproject\volumedata-${product}:/data `
-p 7474:7474 `
-p 7687:7687 `
--memory="7g" `
neo4j:latestmake sure you have in your .gitignore:
volumedata/
- Neo4j Desktop: Download Neo4j Desktop. Create a new project and local DBMS instance. Use
neo4j://localhost:7687as the connection string and the password you set during setup.
- Copy the
Neo4jLiteRepoproject into your solution (NuGet not created yet). - Copy
.Importerand optionally.Samplefor reference. - Create your own project for node models and services, following the
.Samplestructure. - Configure Neo4j connection in
appsettings.json:
{
"Neo4jSettings": {
"Connection": "neo4j://localhost:7687",
"User": "neo4j",
"Password": "your-password",
"Database": "neo4j",
"DetachDeleteWhitelist": [ "TempNode", "TestData" ],
"TransactionTimeoutSeconds": 120,
"MaxConnectionPoolSize": 100
}
}Security Note: The DetachDeleteWhitelist array specifies which node labels can be deleted using detach delete operations. An empty array (default) prevents accidental mass deletes. Only add labels you explicitly want to allow for bulk deletion.
- Add node classes (inheriting from
GraphNode) and decorate with attributes as described above. - Add node services for each node type.
- Register all node services in your DI container.
- Use the Importer project or your own logic to seed data.
- Relationships are created by matching primary key values in related node lists.
- Use the repository's built-in methods for most queries.
- For custom Cypher, use parameterized queries and prefer
MERGEfor upserts. - Example to view all nodes and relationships:
MATCH (n)
OPTIONAL MATCH (n)-[r]-(m)
RETURN n, r, mNodeTrainer is a utility for generating node classes and node service classes from sample JSON data. This helps automate the creation of models and services that follow Neo4jLiteRepo conventions.
- Reads JSON files from a configured directory (see
Neo4jLiteRepo:TrainingInputDirectoryin yourappsettings.json). - Analyzes the structure of each JSON file to generate C# node classes (inheriting from
GraphNode) and corresponding node service classes. - Generated files are saved under
Node.Training/Generated/NodesandNode.Training/Generated/NodeServices.
- Configure the input directory and property handling in
Node.Training/appsettings.json:Neo4jLiteRepo:TrainingInputDirectory: Directory containing your sample JSON files.AlwaysSeparateNodeProps,AlwaysIncludeProperty,AlwaysExcludeProperty, etc., control how properties are modeled.
- Place your sample JSON files in the configured training directory.
- Run the NodeTrainer app (from the
Node.Trainingproject):dotnet run --project src/Node.Training/Node.Training.csproj
- Generated C# files will appear in the
Node.Training/Generatedfolder. - Review and move generated files into your main node model and service projects as needed.
Tip: Adjust configuration to control which properties are modeled as relationships, which are included/excluded, and naming conventions.
- Use descriptive, unique primary keys for each node type.
- Use UPPERCASE_WITH_UNDERSCORES for relationship names.
- Only model child objects as separate nodes if they are reused, complex, or independently queried.
- Always implement
BuildDisplayName()andGetMainContent()for each node. - Use
CustomEdgesubclasses when relationships need custom properties. - Use C# raw string literals for multi-line strings.
- Prefer idiomatic, readable C# over premature optimization.
- Use repository overloads for efficient batching and atomic operations.
- Use
EnforceUniqueConstraintsForAllGraphNodes()for assembly-scanning constraint setup instead of manually passing services.
- .NET 10.0 target framework
- Neo4j.Driver 5.28.4
- Newtonsoft.Json 13.0.4
- Microsoft.Extensions.Configuration 10.0.0
- Microsoft.Extensions.Logging.Abstractions 10.0.0
- The
.Importerproject uses Serilog for logging (optional). - All Neo4j connection/configuration is handled via
appsettings.json.
Contributions are welcome! Please see our CONTRIBUTING.md for more details.
This project is licensed under the MIT License. See the LICENSE file for details.
If you have any questions, feel free to reach out.
Happy coding!