关于 neo4j-driver-dotnet-skill
面向编写 Neo4j .NET 应用的开发者,提供 Neo4j.Driver 使用指导,涵盖异步生命周期、查询执行、事务、结果映射与错误处理。内容侧重常见陷阱及安全、可靠的驱动实践。
主要功能
- 01IDriver 与会话生命周期指导
- 02查询 API 与事务选择
- 03异步游标和结果处理
- 04Cypher 参数与批处理建议
- 05类型映射和错误处理
适用场景
- 01在 ASP.NET Core 中注册和配置驱动
- 02实现异步查询及事务逻辑
- 03调试游标、映射或异常问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-driver-dotnet-skill安装后在 Claude Code 中输入/neo4j-driver-dotnet-skill确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdreferences/object-mapping.mdperformance.mdtransactions.md详细介绍
适用场景
- 编写连接 Neo4j 的 C# 或 .NET 代码
- 设置
IDriver、DI 注册或会话/事务生命周期 - 询问
ExecutableQuery、IResultCursor、异步模式、结果映射相关问题 - 调试 .NET 中的会话、类型映射、空值安全或错误处理
不适用场景
- 编写/优化 Cypher 查询 →
neo4j-cypher-skill - 从旧版驱动升级 →
neo4j-migration-skill
安装
dotnet add package Neo4j.Driver
| 包 | 用途 |
|---|---|
Neo4j.Driver | 异步 API — 使用此包 |
Neo4j.Driver.Simple | 同步包装器 |
Neo4j.Driver.Reactive | System.Reactive 流 |
驱动生命周期
IDriver — 线程安全、使用连接池,创建成本高。每个应用创建一个。
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 和 IAsyncSession 实现了 IAsyncDisposable — 始终使用 await using,绝不要使用普通的 using。
// ❌ Wrong — synchronous Dispose() may block thread pool
using var driver = GraphDatabase.Driver(uri, auth);
// ✅ Correct
await using var driver = GraphDatabase.Driver(uri, auth);
身份验证选项:AuthTokens.Basic(u, p) / AuthTokens.Bearer(token) / AuthTokens.Kerberos(ticket) / AuthTokens.None
环境变量
从环境变量 / appsettings.json 加载连接配置 — 绝不要硬编码凭据。
// 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";
使用环境变量覆盖(.NET 标准行为):Neo4j__Uri=neo4j+s://...(双下划线 = 冒号分隔符)。绝不要提交包含真实凭据的 appsettings.json — 使用 appsettings.Development.json(已加入 gitignore)或 CI/生产环境中的环境变量。
DI 注册(ASP.NET Core)
将 IDriver 注册为单例 — 绝不要注册为 Scoped 或 Transient。绝不要在 DI 中注册 IAsyncSession。
// 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();
}
}
选择合适的 API
| API | 适用场景 | 自动重试 | 流式处理 |
|---|---|---|---|
driver.ExecutableQuery() | 大多数查询 — 简单的默认选项 | ✅ | ❌ 立即加载 |
session.ExecuteReadAsync/WriteAsync() | 大型结果集、多查询事务 | ✅ | ✅ |
session.RunAsync() | LOAD CSV、CALL {} IN TRANSACTIONS | ❌ | ✅ |
session.BeginTransactionAsync() | 多函数、外部协调 | ❌ | ✅ |
ExecutableQuery — 推荐的默认选项
流式构建器;自动管理会话、事务、重试和书签。
// 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>
绝不要遗漏 await:ExecuteAsync() 返回 Task — 缺少 await 虽然能静默编译,但查询永远不会运行。
绝不要使用字符串插值构造 Cypher。始终使用 WithParameters() — 可防止注入并启用计划缓存。
托管事务
适用于大型结果集(延迟流式处理)或每个事务中执行多个查询的场景。回调在瞬时故障时会自动重试 — 确保其具备幂等性,内部不要产生副作用。
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");
游标规则:
- 在回调内部使用
ToListAsync()或FetchAsync()循环来读取数据 - 从回调中返回游标 → 事务关闭 → 游标失效 → 抛出异常
// ❌ 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 陷阱:
// ❌ 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 循环
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
游标读取方法:
| 方法 | 记录 | 摘要 | 用途 |
|---|---|---|---|
ToListAsync() | ✅ 全部 | ❌ | 需要记录 |
ToListAsync(mapper) | ✅ 映射后的记录 | ❌ | 需要映射后的记录 |
FetchAsync() 循环 | ✅ 每次一条 | ❌ 直到 ConsumeAsync | 大型/延迟读取 |
ConsumeAsync() | ❌ 丢弃 | ✅ | 需要计数器 |
IResultSummary 性能分析 [v6.3.0+]:summary.Profile 已标记为 [Obsolete] → 使用 summary.QueryProfile(IQueryProfile),其 DbHits、Rows、Time、PageCacheHits、PageCacheMisses 的类型均为 long? — null 表示未记录,与 0 不同。使用 summary.HasProfile 进行判断。
| SingleAsync() | ✅ 恰好 1 条 | ❌ | 预期只有一行 |
记录值访问
// 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");
类型映射
| 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 | .ToDateOnly()(.NET 6+) |
DateTime | ZonedDateTime | .ToDateTimeOffset()(毫秒精度) |
LocalDateTime | LocalDateTime | |
Duration | Duration | 若包含月或天,.ToTimeSpan() 会抛出异常 |
UUID | System.Guid | [driver 6.2+、Bolt 6.1、Neo4j 2026.08+];对象映射也支持与 string 相互转换 |
null | null | 使用可空类型 |
ElementId 仅在单个事务内稳定——不要用它跨不同事务执行 MATCH。
// 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 批处理
// ❌ 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();
传递给 UNWIND 的 WithParameters 中的自定义类实例无法序列化——请使用 new object[] { new { ... } } 或 Dictionary<string, object>。
对象映射(预览 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>();
错误处理
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 */ }
先捕获 ClientException,再捕获 Neo4jException——前者是后者的子类;通用处理程序会将其吞掉。
ex.GqlStatus——稳定的 GQL 状态码;优先使用它,而不是对 ex.Code 进行字符串匹配。
显式回滚事务本身也可能抛出异常——请隔离处理:
catch (Exception original)
{
try { await tx.RollbackAsync(); }
catch (Exception ex) { logger.LogError(ex, "Rollback failed"); }
throw;
}
如果 CommitAsync() 抛出网络错误,提交可能已成功,也可能未成功——请使用 MERGE 和唯一约束,设计幂等写入。
常见错误
| 错误 | 修正方法 |
|---|---|
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 | Task 会被静默地忽略 |
async tx => tx.RunAsync(...) 中没有内部 await | 移除 async,直接返回 Task |
在 QueryConfig/AsyncSession 中省略 database | 始终指定——可省去一次往返 |
Web 应用中未传入 CancellationToken | 传递 HttpContext.RequestAborted |
对空图值调用 .As<string>() | 使用 .As<string?>()——非空类型会抛出异常 |
record["key"] 中的键不存在 | 先检查 record.Keys.Contains() |
FetchAsync 循环后访问 cursor.Current | 这是最后一条记录,不是 null——循环后不要使用 |
FetchAsync() 返回 false 后再次调用 | 会抛出异常——停止循环,不要再次调用 |
| 从托管事务回调中返回 cursor | 在回调内使用 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() 遇到月或天 | 仅对纯秒/纳秒时长安全 |
先捕获 Neo4jException,再捕获 ClientException | ClientException 是其子类——应先捕获它 |
参考
按需加载:
- references/transactions.md — 显式事务、
BeginTransactionAsync、回滚、提交状态不确定、TransactionConfig(超时、元数据)、因果一致性和书签 - references/performance.md — 空间类型(Point/WGS-84/Cartesian)、连接池调优、
WithFetchSize、会话配置选项、CancellationToken用法、大型结果流式处理 - references/object-mapping.md —
AsObject<T>、蓝图映射、lambda 映射、AsObjectsAsync<T>、仓储模式示例
检查清单
- 在 DI 中将
IDriver注册为单例(或在短生命周期应用中使用await using) - 对驱动和会话使用
await using(不要使用普通的using) - 在
QueryConfig/AsyncSession配置中指定database - 简单查询使用
ExecutableQuery;流式处理/多查询使用ExecuteReadAsync/ExecuteWriteAsync - 在托管事务回调中使用游标(不要将其返回)
- 对任何可能为 null 的图值使用可空类型(
string?、int?) - 使用
WithParameters()(不要进行字符串插值) - 使用匿名类型进行 UNWIND 批处理(不要使用自定义类实例)
- 在 Web 应用处理程序中传递
CancellationToken - 在捕获
Neo4jException前先捕获ClientException - 写入操作保持幂等(
MERGE+ 约束),以确保重试安全 - 不要在
ExecuteReadAsync/ExecuteWriteAsync回调中执行副作用