neo4j-driver-dotnet-skill

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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
  • 编写连接Neo4j的C#或.NET代码
  • 配置
    IDriver
    、DI注册,或会话/事务生命周期管理
  • 关于
    ExecutableQuery
    IResultCursor
    、异步模式、结果映射的问题
  • .NET环境下的会话调试、类型映射、空值安全或错误处理

When NOT to Use

不适用场景

  • Writing/optimizing Cypher queries
    neo4j-cypher-skill
  • Upgrading from older driver version
    neo4j-migration-skill

  • 编写/优化Cypher查询 → 使用
    neo4j-cypher-skill
  • 从旧版本驱动升级 → 使用
    neo4j-migration-skill

Install

安装

bash
dotnet add package Neo4j.Driver
PackageUse
Neo4j.Driver
Async API — use this
Neo4j.Driver.Simple
Synchronous wrapper
Neo4j.Driver.Reactive
System.Reactive streams

bash
dotnet add package Neo4j.Driver
包名用途
Neo4j.Driver
异步API — 推荐使用
Neo4j.Driver.Simple
同步封装
Neo4j.Driver.Reactive
System.Reactive流处理

Driver Lifecycle

驱动生命周期

IDriver
— thread-safe, connection-pooled, expensive to create. Create one per application.
csharp
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
.
csharp
// ❌ 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

IDriver
— 线程安全、连接池化、创建成本高。每个应用仅创建一个实例
csharp
using Neo4j.Driver;

// URI 格式:
//   neo4j+s://xxx.databases.neo4j.io   — TLS + 集群路由(Aura)
//   neo4j://localhost                   — 未加密 + 集群路由
//   bolt+s://localhost:7687             — TLS + 单实例
//   bolt://localhost:7687               — 未加密 + 单实例

await using var driver = GraphDatabase.Driver(
    "neo4j+s://xxx.databases.neo4j.io",
    AuthTokens.Basic("neo4j", "password"));

await driver.VerifyConnectivityAsync();   // 启动时快速验证连接
IDriver
IAsyncSession
实现了
IAsyncDisposable
— 务必使用
await using
,绝不要用普通
using
csharp
// ❌ 错误写法 — 同步Dispose()可能阻塞线程池
using var driver = GraphDatabase.Driver(uri, auth);

// ✅ 正确写法
await using var driver = GraphDatabase.Driver(uri, auth);
认证选项:
AuthTokens.Basic(u, p)
/
AuthTokens.Bearer(token)
/
AuthTokens.Kerberos(ticket)
/
AuthTokens.None

Environment Variables

环境变量

Load connection config from environment /
appsettings.json
— never hardcode credentials.
json
// appsettings.json
{
  "Neo4j": {
    "Uri": "neo4j+s://xxx.databases.neo4j.io",
    "User": "neo4j",
    "Password": "secret",
    "Database": "neo4j"
  }
}
csharp
// 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.

从环境变量或
appsettings.json
加载连接配置 — 绝不要硬编码凭证。
json
// appsettings.json
{
  "Neo4j": {
    "Uri": "neo4j+s://xxx.databases.neo4j.io",
    "User": "neo4j",
    "Password": "secret",
    "Database": "neo4j"
  }
}
csharp
// 通过IConfiguration访问(在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";
使用环境变量覆盖配置(标准.NET行为):
Neo4j__Uri=neo4j+s://...
(双下划线对应冒号分隔符)。绝不要提交包含真实凭证的
appsettings.json
— 使用
appsettings.Development.json
(已加入git忽略)或CI/生产环境中的环境变量。

DI Registration (ASP.NET Core)

DI注册(ASP.NET Core)

Register
IDriver
as singleton — never Scoped or Transient. Never register
IAsyncSession
in DI.
csharp
// 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();
    }
}

IDriver
注册为单例 — 绝不要注册为Scoped或Transient。绝不要在DI中注册
IAsyncSession
csharp
// Program.cs
builder.Services.AddSingleton<IDriver>(_ =>
    GraphDatabase.Driver(
        builder.Configuration["Neo4j:Uri"],
        AuthTokens.Basic(
            builder.Configuration["Neo4j:User"],
            builder.Configuration["Neo4j:Password"])));

// 关闭钩子 — 优雅释放单例
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;
}

// 注入到服务中 — 每个工作单元打开一个会话
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

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

API适用场景自动重试流式处理
driver.ExecutableQuery()
大多数查询 — 简单默认选项❌ 立即加载
session.ExecuteReadAsync/WriteAsync()
大结果集、多查询事务
session.RunAsync()
LOAD CSV
CALL {} IN TRANSACTIONS
session.BeginTransactionAsync()
多功能、外部协调事务

ExecutableQuery — Recommended Default

ExecutableQuery — 推荐默认选项

Fluent builder; manages session, transaction, retries, and bookmarks automatically.
csharp
// 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.

流式构建器;自动管理会话、事务、重试和书签。
csharp
// 读操作
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))    // 将读操作路由到副本
    .ExecuteAsync(cancellationToken);

foreach (var r in records)
    Console.WriteLine(r.Get<string>("name"));

// 使用ResultConsumedAfter统计总耗时(ResultAvailableAfter仅统计首字节响应时间)
Console.WriteLine($"{summary.ResultConsumedAfter.TotalMilliseconds} ms");

// 写操作
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 — 内联映射
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 为 IReadOnlyList<string>
绝不要省略
await
ExecuteAsync()
返回
Task
— 遗漏
await
会编译通过但查询永远不会执行。
绝不要字符串拼接Cypher语句。务必使用
WithParameters()
— 防止注入攻击,启用计划缓存。

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.
csharp
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
csharp
// ❌ 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:
csharp
// ❌ 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" }));

适用于大结果集(延迟流式处理)或单事务内多查询场景。回调函数在临时故障时自动重试 — 确保回调函数是幂等的,内部无副作用。
csharp
await using var session = driver.AsyncSession(conf => conf.WithDatabase("neo4j"));

// 读操作 — 路由到副本
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"));
    // 务必在回调函数内消费游标 — 回调返回后游标失效
});

// 写操作 — 无需异步
await session.ExecuteWriteAsync(tx =>
    tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Carol" }));

// 写操作 — 需要统计信息时使用异步
var summary = await session.ExecuteWriteAsync(async tx =>
{
    var cursor = await tx.RunAsync(
        "CREATE (p:Person {name: $name})", new { name = "Alice" });
    return await cursor.ConsumeAsync();   // 耗尽游标,返回IResultSummary
});
Console.WriteLine($"Created {summary.Counters.NodesCreated} nodes");
游标规则:
  • 使用
    ToListAsync()
    FetchAsync()
    循环在回调函数内消费游标
  • 从回调函数返回游标 → 事务关闭 → 游标失效 → 抛出异常
csharp
// ❌ 返回游标 — lambda返回后事务立即关闭
var cursor = await session.ExecuteReadAsync(async tx =>
    await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name"));
await cursor.FetchAsync();   // 抛出异常

// ✅ 在回调内消费
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"));
});
异步void陷阱:
csharp
// ❌ CS1998警告 — 异步方法无await;RunAsync任务被丢弃
await session.ExecuteWriteAsync(async tx =>
    tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Alice" }));

// ✅ 移除async,直接返回Task
await session.ExecuteWriteAsync(tx =>
    tx.RunAsync("MERGE (p:Person {name: $name})", new { name = "Alice" }));

FetchAsync Loop

FetchAsync循环

csharp
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

csharp
var cursor = await tx.RunAsync("MATCH (p:Person) RETURN p.name AS name");

while (await cursor.FetchAsync())          // 有记录时返回true
{
    Process(cursor.Current.Get<string>("name"));
}
// 循环结束后不要使用cursor.Current — 它保存最后一条记录,而非null
// FetchAsync()返回false后不要再次调用 — 会抛出InvalidOperationException
游标消费方法:
方法记录统计信息用途
ToListAsync()
✅ 全部需要所有记录
ToListAsync(mapper)
✅ 映射后需要映射后的记录
FetchAsync()
循环
✅ 逐条/按需❌ 直到调用ConsumeAsync大结果集/延迟加载
ConsumeAsync()
❌ 丢弃需要统计计数器
SingleAsync()
✅ 仅一条预期仅返回一行

Record Value Access

记录值访问

csharp
// 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");

csharp
// 两种等效写法 — 推荐使用.Get<T>()
string name = record.Get<string>("name");
int    age  = record.Get<int>("age");

string name2 = record["name"].As<string>();   // 索引器 + As<T>
string name3 = record[0].As<string>();        // 按列索引访问

// 空值安全 — 对null图值调用.As<T>()会抛出InvalidCastException
string? city = record["city"].As<string?>();  // ✅ 可空类型
int?    age2 = record["age"].As<int?>();      // ✅ 可空类型

// 不存在的键 — 抛出KeyNotFoundException(拼写错误或未在RETURN中包含)
if (record.Keys.Contains("city"))
    var city3 = record.Get<string?>("city");

Type Mapping

类型映射

Cypher.NET defaultNotes
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.
csharp
// 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();

Cypher类型.NET默认类型说明
Integer
long
兼容:
int
,
long?
,
int?
Float
double
兼容:
float
,
double?
String
string
可为空时使用
string?
Boolean
bool
List
IReadOnlyList<object>
Map
IReadOnlyDictionary<string,object>
Node
INode
包含
.Labels
,
.Properties
,
.ElementId
Relationship
IRelationship
包含
.Type
,
.StartNodeElementId
Date
LocalDate
.NET 6+可调用
.ToDateOnly()
DateTime
ZonedDateTime
可调用
.ToDateTimeOffset()
(毫秒精度)
LocalDateTime
LocalDateTime
Duration
Duration
包含月/日时调用
.ToTimeSpan()
会抛出异常
null
null
使用可空类型
ElementId
仅在单个事务内稳定 — 不要跨事务使用它进行MATCH操作。
csharp
// 传递CLR类型作为参数 — 驱动会自动转换
await driver.ExecutableQuery("CREATE (e:Event {at: $ts})")
    .WithParameters(new { ts = DateTimeOffset.UtcNow })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

UNWIND Batching

UNWIND批量处理

csharp
// ❌ 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>
.

csharp
// ❌ 每条记录一个事务 — 开销巨大
foreach (var item in items)
    await driver.ExecutableQuery("MERGE (n:Node {id: $id})")
        .WithParameters(new { id = item.Id })
        .WithConfig(new QueryConfig(database: "neo4j"))
        .ExecuteAsync();

// ✅ 通过UNWIND实现单事务批量处理 — 仅支持匿名类型(自定义类无法序列化)
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();
传递给
WithParameters
用于UNWIND的自定义类实例无法序列化 — 使用
new object[] { new { ... } }
Dictionary<string, object>

Object Mapping (Preview API)

对象映射(预览API)

csharp
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>();

csharp
using Neo4j.Driver.Preview.Mapping;   // 必须添加 — 否则AsObject<T>()会报CS1061错误

public record Person(string Name, int Age);   // C# record适配性良好

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键映射到record属性

// 批量映射
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

错误处理

csharp
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:
csharp
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.

csharp
try
{
    await driver.ExecutableQuery("...")
        .WithConfig(new QueryConfig(database: "neo4j"))
        .ExecuteAsync();
}
catch (AuthenticationException ex)      { /* 凭证错误 */ }
catch (ServiceUnavailableException ex)  { /* 数据库不可达 */ }
catch (ClientException ex)
    when (ex.Code == "Neo.ClientError.Schema.ConstraintValidationFailed")
{
    // 唯一/存在约束违反 — 需在Neo4jException之前捕获
}
catch (Neo4jException ex)               { /* 其他所有服务器错误 */ }
务必在
Neo4jException
之前捕获
ClientException
— 它是子类,通用处理会覆盖它。
ex.GqlStatus
— 稳定的GQL状态码;优先使用而非字符串匹配
ex.Code
显式事务回滚本身可能抛出异常 — 单独隔离处理:
csharp
catch (Exception original)
{
    try { await tx.RollbackAsync(); }
    catch (Exception ex) { logger.LogError(ex, "Rollback failed"); }
    throw;
}
如果
CommitAsync()
抛出网络错误,提交可能成功也可能失败 — 使用
MERGE
+唯一约束设计幂等写操作。

Common Mistakes

常见错误

MistakeFix
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 callbackConsume 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 callbackMove 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

错误写法修复方案
using var driver
await using var driver
IDriver
实现了
IAsyncDisposable
using var session
await using var session
DI中
IDriver
注册为Scoped/Transient
注册为Singleton
DI中注册
IAsyncSession
绝不要 — 每个工作单元单独打开
ExecuteAsync()
遗漏
await
任务会静默不执行
async tx => tx.RunAsync(...)
内部无await
移除
async
,直接返回Task
QueryConfig
/
AsyncSession
中省略
database
务必指定 — 减少一次网络往返
Web应用中未使用
CancellationToken
传递
HttpContext.RequestAborted
对null图值调用
.As<string>()
使用
.As<string?>()
— 非可空类型会抛出异常
record["key"]
访问不存在的键
先检查
record.Keys.Contains()
FetchAsync循环后使用
cursor.Current
保存的是最后一条记录而非null — 循环结束后不要使用
FetchAsync()返回false后再次调用会抛出异常 — 停止循环,不要再次调用
从托管事务回调返回游标在回调内使用
ToListAsync()
消费
会话写操作需要统计信息使用
await cursor.ConsumeAsync()
AsObject<T>()
报CS1061编译错误
添加
using Neo4j.Driver.Preview.Mapping;
使用
ResultAvailableAfter
统计总耗时
使用
ResultConsumedAfter
(完整耗时)
UNWIND时
WithParameters
使用自定义类
使用匿名类型或
Dictionary<string,object>
修改C#参数名但未修改Cypher中的
$param
匿名属性名必须与
$param
名匹配
读操作使用
ExecuteWriteAsync
使用
ExecuteReadAsync
— 路由到副本
托管事务回调内有副作用移到外部 — 回调在故障时会重试
包含月/日的
Duration.ToTimeSpan()
仅适用于纯秒/纳秒的Duration
ClientException
之前捕获
Neo4jException
ClientException
是子类 — 优先捕获

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.md
    AsObject<T>
    , blueprint mapping, lambda mapping,
    AsObjectsAsync<T>
    , repository pattern example

按需加载:
  • references/transactions.md — 显式事务、
    BeginTransactionAsync
    、回滚、提交不确定性、
    TransactionConfig
    (超时、元数据)、因果一致性和书签
  • references/performance.md — 空间类型(Point/WGS-84/Cartesian)、连接池调优、
    WithFetchSize
    、会话配置选项、
    CancellationToken
    模式、大结果集流式处理
  • references/object-mapping.md
    AsObject<T>
    、蓝图映射、Lambda映射、
    AsObjectsAsync<T>
    、仓储模式示例

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
  • IDriver
    在DI中注册为单例(短期应用使用
    await using
  • 驱动和会话使用
    await using
    (而非普通
    using
  • QueryConfig
    /
    AsyncSession
    配置中指定了
    database
  • 简单查询使用
    ExecutableQuery
    ;流式/多查询使用
    ExecuteReadAsync
    /
    ExecuteWriteAsync
  • 托管事务回调内消费游标(不返回)
  • 可能为null的图值使用可空类型(
    string?
    ,
    int?
  • 使用
    WithParameters()
    (不使用字符串拼接)
  • UNWIND批量处理使用匿名类型(而非自定义类实例)
  • Web应用处理程序中传递
    CancellationToken
  • Neo4jException
    之前捕获
    ClientException
  • 写操作设计为幂等(
    MERGE
    +约束)以支持重试
  • ExecuteReadAsync
    /
    ExecuteWriteAsync
    回调内无副作用