Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
neo4j-contrib avatar

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-skill

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs325
repo stars101
Security audit3 / 3 scanners passed
Last updatedAugust 3, 2026
Repositoryneo4j-contrib/neo4j-skills

What it does

Implement correct Neo4j.NET Driver v6 patterns in C# apps—sessions, transactions, mapping, and batching.

Files

SKILL.mdMarkdownGitHub ↗

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 queriesneo4j-cypher-skill
  • Upgrading from older driver versionneo4j-migration-skill

---

Install

dotnet add package Neo4j.Driver
PackageUse
Neo4j.DriverAsync API — use this
Neo4j.Driver.SimpleSynchronous wrapper
Neo4j.Driver.ReactiveSystem.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 startup

IDriver 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

APIWhenAuto-retryStreaming
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() or FetchAsync() 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 InvalidOperationException

Cursor consumption methods:

MethodRecordsSummaryUse
ToListAsync()✅ allNeed records
ToListAsync(mapper)✅ mappedNeed mapped records
FetchAsync() loop✅ one/time❌ until ConsumeAsyncLarge/lazy
ConsumeAsync()❌ discardsNeed counters
SingleAsync()✅ exactly 1Expect 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 defaultNotes
Integerlongsafe: int, long?, int?
Floatdoublesafe: float, double?
Stringstringuse string? if nullable
Booleanbool
ListIReadOnlyList<object>
MapIReadOnlyDictionary<string,object>
NodeINode.Labels, .Properties, .ElementId
RelationshipIRelationship.Type, .StartNodeElementId
DateLocalDate.ToDateOnly() (.NET 6+)
DateTimeZonedDateTime.ToDateTimeOffset() (ms precision)
LocalDateTimeLocalDateTime
DurationDuration.ToTimeSpan() throws if has months/days
nullnulluse 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

MistakeFix
using var driverawait using var driverIDriver is IAsyncDisposable
using var sessionawait using var session
IDriver as Scoped/Transient in DIRegister as Singleton
IAsyncSession in DINever — open per unit of work
Missing await on ExecuteAsync()Task silently never runs
async tx => tx.RunAsync(...) no inner awaitRemove async, return Task directly
Omit database in QueryConfig/AsyncSessionAlways specify — saves a round-trip
No CancellationToken in web appsPropagate HttpContext.RequestAborted
.As<string>() on null graph value.As<string?>() — non-nullable throws
record["key"] absent keyCheck record.Keys.Contains() first
cursor.Current after FetchAsync loopLast record, not null — don't use after loop
FetchAsync() after false returnThrows — stop loop, don't call again
Return cursor from managed tx callbackConsume with ToListAsync() inside callback
Need counters from session writeawait cursor.ConsumeAsync()
AsObject<T>() CS1061 compile errorAdd using Neo4j.Driver.Preview.Mapping;
ResultAvailableAfter for total timingUse ResultConsumedAfter (full wall-clock)
Custom class in WithParameters for UNWINDUse anonymous types or Dictionary<string,object>
Rename C# param but not Cypher $paramAnonymous property names must match $param names
ExecuteWriteAsync for readsUse ExecuteReadAsync — routes to replicas
Side effects inside managed tx callbackMove outside — callback retried on failure
Duration.ToTimeSpan() with months/daysOnly safe for pure second/nanosecond durations
Catch Neo4jException before ClientExceptionClientException 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, CancellationToken patterns, large result streaming
  • references/object-mapping.mdAsObject<T>, blueprint mapping, lambda mapping, AsObjectsAsync<T>, repository pattern example

---

Checklist

  • [ ] IDriver registered as singleton in DI (or await using for short-lived apps)
  • [ ] await using on driver and sessions (not plain using)
  • [ ] database specified in QueryConfig / AsyncSession config
  • [ ] ExecutableQuery used for simple queries; ExecuteReadAsync/ExecuteWriteAsync for 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)
  • [ ] CancellationToken propagated in web app handlers
  • [ ] ClientException caught before Neo4jException
  • [ ] Writes idempotent (MERGE + constraints) for retry safety
  • [ ] No side effects inside ExecuteReadAsync/ExecuteWriteAsync callbacks

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.

Databasesbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.