
Neo4j Driver Dotnet Skill
- 325 installs
- 101 repo stars
- Updated August 3, 2026
- neo4j-contrib/neo4j-skills
Implement correct Neo4j.NET Driver v6 patterns in C# apps—sessions, transactions, mapping, and batching.
About
Neo4j Driver Dotnet Skill is a reference-style agent skill that keeps solo .NET builders from guessing Neo4j.Driver v6 APIs when shipping graph-backed features. It documents installation, singleton driver lifecycle with VerifyConnectivityAsync, dependency-injection registration, and when to choose ExecutableQuery versus ExecuteReadAsync and ExecuteWriteAsync managed transactions. Coverage extends through IResultCursor consumption, safe record access, Cypher-to-.NET type mapping including temporal types and ElementId caveats, UNWIND batching with dictionaries or anonymous types, and Preview-level AsObject mapping. Error handling emphasizes exception hierarchy and rollback-safe patterns. The skill deliberately points Cypher authoring to neo4j-cypher-skill and upgrades to neo4j-migration-skill so this package stays focused on connection and execution correctness. Use it while building APIs, workers, or internal tools that persist relationships in Neo4j; skip it if you are on another language driver or not using Neo4j at all.
- Official Neo4j.Driver NuGet guidance for v6 on .NET 8/9/10
- Decision table for ExecutableQuery vs managed vs explicit transactions
- DI recipes: AddSingleton IDriver, session-per-unit-of-work, shutdown hooks
- 20+ common mistake/fix pairs including async void and ClientException ordering
- UNWIND batching, record access, temporal mapping, and Preview object mapping APIs
Neo4j Driver Dotnet Skill by the numbers
- 325 all-time installs (skills.sh)
- +25 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #169 of 911 Databases skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-driver-dotnet-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 325 |
|---|---|
| repo stars | ★ 101 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | neo4j-contrib/neo4j-skills ↗ |
What it does
Implement correct Neo4j.NET Driver v6 patterns in C# apps—sessions, transactions, mapping, and batching.
Files
When to Use
- Writing C# or .NET code connecting to Neo4j
- Setting up
IDriver, DI registration, or session/transaction lifecycle - Questions about
ExecutableQuery,IResultCursor, async patterns, result mapping - Debugging sessions, type mapping, null safety, or error handling in .NET
When NOT to Use
- Writing/optimizing Cypher queries →
neo4j-cypher-skill - Upgrading from older driver version →
neo4j-migration-skill
---
Install
dotnet add package Neo4j.Driver| Package | Use |
|---|---|
Neo4j.Driver | Async API — use this |
Neo4j.Driver.Simple | Synchronous wrapper |
Neo4j.Driver.Reactive | System.Reactive streams |
---
Driver Lifecycle
IDriver — thread-safe, connection-pooled, expensive to create. Create one per application.
using Neo4j.Driver;
// URI schemes:
// neo4j+s://xxx.databases.neo4j.io — TLS + cluster routing (Aura)
// neo4j://localhost — unencrypted + cluster routing
// bolt+s://localhost:7687 — TLS + single instance
// bolt://localhost:7687 — unencrypted + single instance
await using var driver = GraphDatabase.Driver(
"neo4j+s://xxx.databases.neo4j.io",
AuthTokens.Basic("neo4j", "password"));
await driver.VerifyConnectivityAsync(); // fail fast on startupIDriver and IAsyncSession implement IAsyncDisposable — always await using, never plain using.
// ❌ Wrong — synchronous Dispose() may block thread pool
using var driver = GraphDatabase.Driver(uri, auth);
// ✅ Correct
await using var driver = GraphDatabase.Driver(uri, auth);Auth options: AuthTokens.Basic(u, p) / AuthTokens.Bearer(token) / AuthTokens.Kerberos(ticket) / AuthTokens.None
---
Environment Variables
Load connection config from environment / appsettings.json — never hardcode credentials.
// appsettings.json
{
"Neo4j": {
"Uri": "neo4j+s://xxx.databases.neo4j.io",
"User": "neo4j",
"Password": "secret",
"Database": "neo4j"
}
}// Access via IConfiguration (injected in Program.cs)
var uri = builder.Configuration["Neo4j:Uri"];
var user = builder.Configuration["Neo4j:User"];
var password = builder.Configuration["Neo4j:Password"];
var database = builder.Configuration["Neo4j:Database"] ?? "neo4j";Override with environment variables (standard .NET behavior): Neo4j__Uri=neo4j+s://... (double underscore = colon separator). Never commit appsettings.json with real credentials — use appsettings.Development.json (gitignored) or env vars in CI/production.
---
DI Registration (ASP.NET Core)
Register IDriver as singleton — never Scoped or Transient. Never register IAsyncSession in DI.
// Program.cs
builder.Services.AddSingleton<IDriver>(_ =>
GraphDatabase.Driver(
builder.Configuration["Neo4j:Uri"],
AuthTokens.Basic(
builder.Configuration["Neo4j:User"],
builder.Configuration["Neo4j:Password"])));
// Shutdown hook — dispose the singleton cleanly
builder.Services.AddHostedService<Neo4jShutdownService>();
// Neo4jShutdownService.cs
public class Neo4jShutdownService(IDriver driver, IHostApplicationLifetime lifetime)
: IHostedService
{
public Task StartAsync(CancellationToken _)
{
lifetime.ApplicationStopping.Register(() =>
driver.DisposeAsync().AsTask().GetAwaiter().GetResult());
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken _) => Task.CompletedTask;
}
// Inject into services — sessions opened per unit of work
public class PersonService(IDriver driver)
{
public async Task<List<string>> GetNamesAsync(CancellationToken ct = default)
{
var (records, _, _) = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync(ct);
return records.Select(r => r.Get<string>("name")).ToList();
}
}---
Choose the Right API
| API | When | Auto-retry | Streaming |
|---|---|---|---|
driver.ExecutableQuery() | Most queries — simple default | ✅ | ❌ eager |
session.ExecuteReadAsync/WriteAsync() | Large results, multi-query tx | ✅ | ✅ |
session.RunAsync() | LOAD CSV, CALL {} IN TRANSACTIONS | ❌ | ✅ |
session.BeginTransactionAsync() | Multi-function, external coordination | ❌ | ✅ |
---
ExecutableQuery — Recommended Default
Fluent builder; manages session, transaction, retries, and bookmarks automatically.
// Read
var (records, summary, keys) = await driver
.ExecutableQuery("MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name")
.WithParameters(new { name = "Alice" })
.WithConfig(new QueryConfig(
database: "neo4j",
routing: RoutingControl.Readers)) // route reads to replicas
.ExecuteAsync(cancellationToken);
foreach (var r in records)
Console.WriteLine(r.Get<string>("name"));
// Use ResultConsumedAfter for wall-clock timing (ResultAvailableAfter = time-to-first-byte only)
Console.WriteLine($"{summary.ResultConsumedAfter.TotalMilliseconds} ms");
// Write
var (_, writeSummary, _) = await driver
.ExecutableQuery("CREATE (p:Person {name: $name, age: $age})")
.WithParameters(new { name = "Bob", age = 30 })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
Console.WriteLine($"Created {writeSummary.Counters.NodesCreated} nodes");
// WithMap — project inline
var names = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithMap(r => r["name"].As<string>())
.ExecuteAsync(); // names.Result is IReadOnlyList<string>Never await omitted: ExecuteAsync() returns Task — missing await compiles silently but query never runs.
Never string-interpolate Cypher. Always WithParameters() — prevents injection, enables plan caching.
---
Managed Transactions
Use for large result sets (lazy streaming) or multiple queries per transaction. Callback auto-retried on transient failure — keep it idempotent, no side effects inside.
await using var session = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
// Read — routes to replicas
var names = await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync(
"MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name",
new { prefix = "Al" });
return await cursor.ToListAsync(r => r.Get<string>("name"));
// Consume cursor INSIDE callback — invalid after callback returns
});
// Write — void, no async needed
await session.ExecuteWriteAsync(tx =>
tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Carol" }));
// Write — async when needing counters
var summary = await session.ExecuteWriteAsync(async tx =>
{
var cursor = await tx.RunAsync(
"CREATE (p:Person {name: $name})", new { name = "Alice" });
return await cursor.ConsumeAsync(); // drains cursor, returns IResultSummary
});
Console.WriteLine($"Created {summary.Counters.NodesCreated} nodes");Cursor rules:
- Consume with
ToListAsync()orFetchAsync()loop inside the callback - Returning a cursor from the callback → transaction closes → cursor invalid → exception
// ❌ Returns cursor — tx closes immediately after lambda returns
var cursor = await session.ExecuteReadAsync(async tx =>
await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name"));
await cursor.FetchAsync(); // throws
// ✅ Consume inside
var names = await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name");
return await cursor.ToListAsync(r => r.Get<string>("name"));
});Async void trap:
// ❌ CS1998 warning — async with no await; RunAsync Task discarded
await session.ExecuteWriteAsync(async tx =>
tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Alice" }));
// ✅ No async, return Task directly
await session.ExecuteWriteAsync(tx =>
tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Alice" }));---
FetchAsync Loop
var cursor = await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name");
while (await cursor.FetchAsync()) // true while records remain
{
Process(cursor.Current.Get<string>("name"));
}
// Do NOT use cursor.Current after the loop — it holds the last record, not null
// Do NOT call FetchAsync() again after it returned false — throws InvalidOperationExceptionCursor consumption methods:
| Method | Records | Summary | Use |
|---|---|---|---|
ToListAsync() | ✅ all | ❌ | Need records |
ToListAsync(mapper) | ✅ mapped | ❌ | Need mapped records |
FetchAsync() loop | ✅ one/time | ❌ until ConsumeAsync | Large/lazy |
ConsumeAsync() | ❌ discards | ✅ | Need counters |
SingleAsync() | ✅ exactly 1 | ❌ | Expect one row |
---
Record Value Access
// Two equivalent patterns — prefer .Get<T>()
string name = record.Get<string>("name");
int age = record.Get<int>("age");
string name2 = record["name"].As<string>(); // indexer + As<T>
string name3 = record[0].As<string>(); // by column index
// Null safety — .As<T>() on null graph value throws InvalidCastException
string? city = record["city"].As<string?>(); // ✅ nullable
int? age2 = record["age"].As<int?>(); // ✅ nullable
// Absent key — throws KeyNotFoundException (typo or not in RETURN)
if (record.Keys.Contains("city"))
var city3 = record.Get<string?>("city");---
Type Mapping
| Cypher | .NET default | Notes |
|---|---|---|
Integer | long | safe: int, long?, int? |
Float | double | safe: float, double? |
String | string | use string? if nullable |
Boolean | bool | |
List | IReadOnlyList<object> | |
Map | IReadOnlyDictionary<string,object> | |
Node | INode | .Labels, .Properties, .ElementId |
Relationship | IRelationship | .Type, .StartNodeElementId |
Date | LocalDate | .ToDateOnly() (.NET 6+) |
DateTime | ZonedDateTime | .ToDateTimeOffset() (ms precision) |
LocalDateTime | LocalDateTime | |
Duration | Duration | .ToTimeSpan() throws if has months/days |
null | null | use nullable types |
ElementId stable within one transaction only — do not use to MATCH across separate transactions.
// Pass CLR types as params — driver converts automatically
await driver.ExecutableQuery("CREATE (e:Event {at: $ts})")
.WithParameters(new { ts = DateTimeOffset.UtcNow })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();---
UNWIND Batching
// ❌ One transaction per record — high overhead
foreach (var item in items)
await driver.ExecutableQuery("MERGE (n:Node {id: $id})")
.WithParameters(new { id = item.Id })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
// ✅ Single transaction via UNWIND — anonymous types only (custom classes don't serialize)
var rows = items.Select(i => new { id = i.Id, name = i.Name }).ToArray();
await driver.ExecutableQuery(@"
UNWIND $rows AS row
MERGE (n:Node {id: row.id})
SET n.name = row.name")
.WithParameters(new { rows })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();Custom class instances passed to WithParameters for UNWIND do not serialize — use new object[] { new { ... } } or Dictionary<string, object>.
---
Object Mapping (Preview API)
using Neo4j.Driver.Preview.Mapping; // REQUIRED — without this, AsObject<T>() is CS1061
public record Person(string Name, int Age); // C# records work well here
var result = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name, p.age AS age")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
var person = result.Result[0].AsObject<Person>(); // RETURN keys map to record properties
// Bulk mapping
var (people, _, _) = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name, p.age AS age")
.WithConfig(new QueryConfig(database: "neo4j"))
.AsObjectsAsync<Person>();---
Error Handling
try
{
await driver.ExecutableQuery("...")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
}
catch (AuthenticationException ex) { /* bad credentials */ }
catch (ServiceUnavailableException ex) { /* database unreachable */ }
catch (ClientException ex)
when (ex.Code == "Neo.ClientError.Schema.ConstraintValidationFailed")
{
// Unique/existence constraint violation — catch BEFORE Neo4jException
}
catch (Neo4jException ex) { /* all other server errors */ }Catch ClientException before Neo4jException — it's a subclass; generic handler swallows it.
ex.GqlStatus — stable GQL status codes; prefer over string-matching ex.Code.
Explicit transaction rollback can itself throw — isolate it:
catch (Exception original)
{
try { await tx.RollbackAsync(); }
catch (Exception ex) { logger.LogError(ex, "Rollback failed"); }
throw;
}If CommitAsync() throws a network error, commit may or may not have succeeded — design writes idempotent with MERGE + unique constraints.
---
Common Mistakes
| Mistake | Fix |
|---|---|
using var driver | await using var driver — IDriver is IAsyncDisposable |
using var session | await using var session |
IDriver as Scoped/Transient in DI | Register as Singleton |
IAsyncSession in DI | Never — open per unit of work |
Missing await on ExecuteAsync() | Task silently never runs |
async tx => tx.RunAsync(...) no inner await | Remove async, return Task directly |
Omit database in QueryConfig/AsyncSession | Always specify — saves a round-trip |
No CancellationToken in web apps | Propagate HttpContext.RequestAborted |
.As<string>() on null graph value | .As<string?>() — non-nullable throws |
record["key"] absent key | Check record.Keys.Contains() first |
cursor.Current after FetchAsync loop | Last record, not null — don't use after loop |
FetchAsync() after false return | Throws — stop loop, don't call again |
| Return cursor from managed tx callback | Consume with ToListAsync() inside callback |
| Need counters from session write | await cursor.ConsumeAsync() |
AsObject<T>() CS1061 compile error | Add using Neo4j.Driver.Preview.Mapping; |
ResultAvailableAfter for total timing | Use ResultConsumedAfter (full wall-clock) |
Custom class in WithParameters for UNWIND | Use anonymous types or Dictionary<string,object> |
Rename C# param but not Cypher $param | Anonymous property names must match $param names |
ExecuteWriteAsync for reads | Use ExecuteReadAsync — routes to replicas |
| Side effects inside managed tx callback | Move outside — callback retried on failure |
Duration.ToTimeSpan() with months/days | Only safe for pure second/nanosecond durations |
Catch Neo4jException before ClientException | ClientException is subclass — catch it first |
---
References
Load on demand:
- references/transactions.md — explicit transactions,
BeginTransactionAsync, rollback, commit uncertainty,TransactionConfig(timeout, metadata), causal consistency and bookmarks - references/performance.md — spatial types (Point/WGS-84/Cartesian), connection pool tuning,
WithFetchSize, session config options,CancellationTokenpatterns, large result streaming - references/object-mapping.md —
AsObject<T>, blueprint mapping, lambda mapping,AsObjectsAsync<T>, repository pattern example
---
Checklist
- [ ]
IDriverregistered as singleton in DI (orawait usingfor short-lived apps) - [ ]
await usingon driver and sessions (not plainusing) - [ ]
databasespecified inQueryConfig/AsyncSessionconfig - [ ]
ExecutableQueryused for simple queries;ExecuteReadAsync/ExecuteWriteAsyncfor streaming/multi-query - [ ] Cursor consumed inside managed tx callback (not returned)
- [ ] Nullable types (
string?,int?) on any graph value that can be null - [ ]
WithParameters()used (no string interpolation) - [ ] UNWIND batching with anonymous types (not custom class instances)
- [ ]
CancellationTokenpropagated in web app handlers - [ ]
ClientExceptioncaught beforeNeo4jException - [ ] Writes idempotent (
MERGE+ constraints) for retry safety - [ ] No side effects inside
ExecuteReadAsync/ExecuteWriteAsynccallbacks
neo4j-driver-dotnet-skill
Official Neo4j .NET Driver v6 — usage guide for C# / .NET applications connecting to Neo4j.
Topics covered
- Install —
Neo4j.DriverNuGet package, package variants - Driver lifecycle —
IDriversingleton,await using,VerifyConnectivityAsync - DI registration —
AddSingleton<IDriver>, shutdown hook, session-per-unit-of-work - API selection —
ExecutableQueryvs managed vs explicit transactions decision table - ExecutableQuery — fluent builder,
WithParameters,WithConfig,WithMap,EagerResultdeconstruct - Managed transactions —
ExecuteReadAsync/ExecuteWriteAsync, retry safety, async void trap - IResultCursor —
ToListAsync,FetchAsyncloop,ConsumeAsync,SingleAsync - Record access —
.Get<T>(),.As<T>(), null safety, absent key handling - Type mapping — Cypher → .NET table, temporal types,
ElementIdlifetime - UNWIND batching — anonymous type arrays,
Dictionary<string,object> - Object mapping —
AsObject<T>,AsObjectsAsync<T>, C# record types (Preview API) - Error handling — exception hierarchy,
ClientExceptionordering, rollback safety - Common mistakes — 20+ mistake/fix table
Version / compatibility
Driver v6, .NET 8/9/10. Docs: https://neo4j.com/docs/dotnet-manual/current/
Not covered
- Cypher query authoring →
neo4j-cypher-skill - Driver version upgrades →
neo4j-migration-skill
Install
dotnet add package Neo4j.DriverObject Mapping and Repository Pattern
Preview Mapping API
All mapping extension methods live in Neo4j.Driver.Preview.Mapping. Without this using directive, AsObject<T>() and AsObjectsAsync<T>() cause CS1061 compile errors.
using Neo4j.Driver.Preview.Mapping; // REQUIREDAsObject<T>() — Single Record
public record Person(string Name, int Age);
var result = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name, p.age AS age")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
var person = result.Result[0].AsObject<Person>();
// RETURN key names map to property names (case-insensitive by default)
// 'name' → Name, 'age' → AgeC# record types work well — positional constructor parameters are matched by name.
Blueprint Mapping (anonymous types)
var person = result.Result[0].AsObjectFromBlueprint(new { name = "", age = 0 });
Console.WriteLine(person.name); // "Alice"
Console.WriteLine(person.age); // 21Lambda Mapping
var person = result.Result[0].AsObject(
(string name, int age) => new { Name = name, Age = age, BirthYear = 2025 - age });AsObjectsAsync<T>() — Bulk Mapping
var (people, summary, _) = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name, p.age AS age")
.WithConfig(new QueryConfig(database: "neo4j"))
.AsObjectsAsync<Person>(); // maps all records; returns EagerResult<IReadOnlyList<Person>>---
Repository Pattern Example
public interface IPersonRepository
{
Task<IReadOnlyList<Person>> FindByNamePrefixAsync(string prefix, CancellationToken ct = default);
Task CreateAsync(Person person, CancellationToken ct = default);
Task BulkCreateAsync(IEnumerable<Person> people, CancellationToken ct = default);
}
public class PersonRepository(IDriver driver, string database = "neo4j")
: IPersonRepository
{
public async Task<IReadOnlyList<Person>> FindByNamePrefixAsync(
string prefix, CancellationToken ct = default)
{
var (records, _, _) = await driver
.ExecutableQuery(@"
MATCH (p:Person)
WHERE p.name STARTS WITH $prefix
RETURN p.name AS name, p.age AS age")
.WithParameters(new { prefix })
.WithConfig(new QueryConfig(database, RoutingControl.Readers))
.ExecuteAsync(ct);
return records
.Select(r => new Person(r.Get<string>("name"), r.Get<int>("age")))
.ToList();
}
public async Task CreateAsync(Person person, CancellationToken ct = default)
{
await driver
.ExecutableQuery("CREATE (p:Person {name: $name, age: $age})")
.WithParameters(new { name = person.Name, age = person.Age })
.WithConfig(new QueryConfig(database))
.ExecuteAsync(ct);
}
public async Task BulkCreateAsync(
IEnumerable<Person> people, CancellationToken ct = default)
{
var rows = people
.Select(p => new { name = p.Name, age = p.Age })
.ToArray();
await driver
.ExecutableQuery(@"
UNWIND $rows AS row
MERGE (p:Person {name: row.name})
SET p.age = row.age")
.WithParameters(new { rows })
.WithConfig(new QueryConfig(database))
.ExecuteAsync(ct);
}
}
public record Person(string Name, int Age);Performance, Type Extras — Neo4j .NET Driver
Spatial Types
using Neo4j.Driver;
// Create points — new Point(srid, x, y) / new Point(srid, x, y, z)
var cartesian2d = new Point(7203, 1.23, 4.56); // Cartesian 2D (SRID 7203)
var cartesian3d = new Point(9157, 1.23, 4.56, 7.89); // Cartesian 3D (SRID 9157)
var london = new Point(4326, -0.118092, 51.509865); // WGS-84 2D (lon, lat) (SRID 4326)
var shard = new Point(4979, -0.0865, 51.5045, 310); // WGS-84 3D (SRID 4979)
// Pass as parameter — driver serializes automatically
await driver.ExecutableQuery("CREATE (p:Place {location: $loc})")
.WithParameters(new { loc = london })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
// Read from result
var pt = record.Get<Point>("location");
Console.WriteLine($"X={pt.X} Y={pt.Y} Z={pt.Z} SRID={pt.SrId}");
// For WGS-84: X=longitude, Y=latitude, Z=height (NaN for 2D)
// Distance (same SRID only — different SRIDs return null)
var dist = (await driver
.ExecutableQuery("RETURN point.distance($p1, $p2) AS distance")
.WithParameters(new { p1 = new Point(7203, 1, 1), p2 = new Point(7203, 10, 10) })
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync()).Result[0].Get<double>("distance");SRID: 4326 = WGS-84 2D, 4979 = WGS-84 3D, 7203 = Cartesian 2D, 9157 = Cartesian 3D.
---
Performance — Connection Pool, Streaming, CancellationToken
Always Specify the Database
Omitting database causes an extra network round-trip on every call:
// ExecutableQuery:
.WithConfig(new QueryConfig(database: "neo4j"))
// Session:
driver.AsyncSession(conf => conf.WithDatabase("neo4j"))Route Reads to Replicas
// ExecutableQuery:
.WithConfig(new QueryConfig(database: "neo4j", routing: RoutingControl.Readers))
// Managed transaction — ExecuteReadAsync routes automatically
await session.ExecuteReadAsync(async tx => { ... });Large Results — Lazy Streaming
ExecutableQuery is always eager — fine for moderate result sets.
For large results, stream lazily inside ExecuteReadAsync:
await using var session = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name");
while (await cursor.FetchAsync())
{
ProcessRecord(cursor.Current.Get<string>("name"));
}
});Connection Pool Tuning
await using var driver = GraphDatabase.Driver(uri, auth, conf => conf
.WithMaxConnectionPoolSize(50)
.WithConnectionAcquisitionTimeout(TimeSpan.FromSeconds(30))
.WithMaxConnectionLifetime(TimeSpan.FromHours(1))
.WithConnectionIdleTimeout(TimeSpan.FromMinutes(10)));Default pool size: 100. Reduce if running many app instances to avoid overwhelming the server.
CancellationToken — Propagate End-to-End
Always propagate the request cancellation token in web apps. Without it, abandoned requests keep running on the server, exhausting the connection pool under load.
// ASP.NET Core controller
[HttpGet("people")]
public async Task<IActionResult> GetPeople(CancellationToken cancellationToken)
{
var (records, _, _) = await driver
.ExecutableQuery("MATCH (p:Person) RETURN p.name AS name")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync(cancellationToken);
return Ok(records.Select(r => r.Get<string>("name")));
}
// Session-based
return await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync(
"MATCH (p:Person) RETURN p.name AS name",
cancellationToken: cancellationToken);
return await cursor.ToListAsync(r => r.Get<string>("name"), cancellationToken);
}, cancellationToken: cancellationToken);
// Explicit transaction
await using var tx = await session.BeginTransactionAsync(cancellationToken);
await tx.RunAsync("CREATE (p:Person {name: $name})", new { name }, cancellationToken);
await tx.CommitAsync(cancellationToken);Explicit Transactions, TransactionConfig, and Causal Consistency
Explicit Transactions (BeginTransactionAsync)
Use when a transaction must span multiple methods or coordinate with external systems. Not automatically retried.
await using var session = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));
await using var tx = await session.BeginTransactionAsync();
try
{
await DoPartA(tx);
await DoPartB(tx);
await tx.CommitAsync();
}
catch (Exception original)
{
try { await tx.RollbackAsync(); }
catch (Exception rollbackEx)
{
logger.LogError(rollbackEx, "Rollback failed");
}
throw;
}
static async Task DoPartA(IAsyncTransaction tx)
{
await tx.RunAsync("CREATE (p:Person {name: $name})", new { name = "Alice" });
}RollbackAsync() is a network call — it can throw. Isolate it with its own try/catch to avoid hiding the original exception.
If CommitAsync() throws a network-level exception, the commit may or may not have succeeded — design writes as idempotent using MERGE + unique constraints.
---
TransactionConfig — Timeouts and Metadata
await session.ExecuteReadAsync(
async tx =>
{
var cursor = await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name");
return await cursor.ToListAsync(r => r.Get<string>("name"));
},
conf => conf
.WithTimeout(TimeSpan.FromSeconds(5))
.WithMetadata(new Dictionary<string, object> { { "app", "myService" } })
);Metadata appears in SHOW TRANSACTIONS — useful for tracing slow queries.
---
Session Configuration Options
await using var session = driver.AsyncSession(conf => conf
.WithDatabase("neo4j")
.WithDefaultAccessMode(AccessMode.Read) // manual routing hint
.WithAuthToken(AuthTokens.Basic("user", "pw")) // per-session auth (multi-tenant)
.WithImpersonatedUser("jane") // impersonate without password
.WithFetchSize(500)); // batch size for streaming (default 1000)---
Causal Consistency — Cross-Session Bookmarks
Within a single session, transactions are automatically causally chained. Across sessions, use ExecutableQuery (auto-managed bookmarks) or pass bookmarks explicitly:
Bookmarks bookmarksA, bookmarksB;
await using (var sessionA = driver.AsyncSession(conf => conf.WithDatabase("neo4j")))
{
await sessionA.ExecuteWriteAsync(tx =>
tx.RunAsync("MERGE (p:Person {name: 'Alice'})"));
bookmarksA = sessionA.LastBookmarks;
}
await using (var sessionB = driver.AsyncSession(conf => conf.WithDatabase("neo4j")))
{
await sessionB.ExecuteWriteAsync(tx =>
tx.RunAsync("MERGE (p:Person {name: 'Bob'})"));
bookmarksB = sessionB.LastBookmarks;
}
// sessionC waits until both Alice and Bob are visible
await using var sessionC = driver.AsyncSession(conf => conf
.WithDatabase("neo4j")
.WithBookmarks(bookmarksA, bookmarksB));
await sessionC.ExecuteWriteAsync(tx =>
tx.RunAsync(
"MATCH (a:Person {name:'Alice'}), (b:Person {name:'Bob'}) MERGE (a)-[:KNOWS]->(b)"));Related skills
FAQ
Is Neo4j Driver Dotnet Skill safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.