neo4j-driver-dotnet-skill
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen to Use
适用场景
- Writing C# or .NET code connecting to Neo4j
- Setting up , DI registration, or session/transaction lifecycle
IDriver - Questions about ,
ExecutableQuery, async patterns, result mappingIResultCursor - Debugging sessions, type mapping, null safety, or error handling in .NET
- 编写连接Neo4j的C#或.NET代码
- 配置、DI注册,或会话/事务生命周期管理
IDriver - 关于、
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| Package | Use |
|---|---|
| Async API — use this |
| Synchronous wrapper |
| System.Reactive streams |
bash
dotnet add package Neo4j.Driver| 包名 | 用途 |
|---|---|
| 异步API — 推荐使用 |
| 同步封装 |
| System.Reactive流处理 |
Driver Lifecycle
驱动生命周期
IDrivercsharp
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 startupIDriverIAsyncSessionIAsyncDisposableawait usingusingcsharp
// ❌ 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.NoneIDrivercsharp
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(); // 启动时快速验证连接IDriverIAsyncSessionIAsyncDisposableawait usingusingcsharp
// ❌ 错误写法 — 同步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.NoneEnvironment Variables
环境变量
Load connection config from environment / — never hardcode credentials.
appsettings.jsonjson
// 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): (double underscore = colon separator). Never commit with real credentials — use (gitignored) or env vars in CI/production.
Neo4j__Uri=neo4j+s://...appsettings.jsonappsettings.Development.json从环境变量或加载连接配置 — 绝不要硬编码凭证。
appsettings.jsonjson
// 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行为):(双下划线对应冒号分隔符)。绝不要提交包含真实凭证的 — 使用(已加入git忽略)或CI/生产环境中的环境变量。
Neo4j__Uri=neo4j+s://...appsettings.jsonappsettings.Development.jsonDI Registration (ASP.NET Core)
DI注册(ASP.NET Core)
Register as singleton — never Scoped or Transient. Never register in DI.
IDriverIAsyncSessioncsharp
// 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();
}
}将注册为单例 — 绝不要注册为Scoped或Transient。绝不要在DI中注册。
IDriverIAsyncSessioncsharp
// 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
| API | When | Auto-retry | Streaming |
|---|---|---|---|
| Most queries — simple default | ✅ | ❌ eager |
| Large results, multi-query tx | ✅ | ✅ |
| | ❌ | ✅ |
| Multi-function, external coordination | ❌ | ✅ |
| API | 适用场景 | 自动重试 | 流式处理 |
|---|---|---|---|
| 大多数查询 — 简单默认选项 | ✅ | ❌ 立即加载 |
| 大结果集、多查询事务 | ✅ | ✅ |
| | ❌ | ✅ |
| 多功能、外部协调事务 | ❌ | ✅ |
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 omitted: returns — missing compiles silently but query never runs.
awaitExecuteAsync()TaskawaitNever string-interpolate Cypher. Always — prevents injection, enables plan caching.
WithParameters()流式构建器;自动管理会话、事务、重试和书签。
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>绝不要省略:返回 — 遗漏会编译通过但查询永远不会执行。
awaitExecuteAsync()Taskawait绝不要字符串拼接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 or
ToListAsync()loop inside the callbackFetchAsync() - 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 InvalidOperationExceptionCursor consumption methods:
| Method | Records | Summary | Use |
|---|---|---|---|
| ✅ all | ❌ | Need records |
| ✅ mapped | ❌ | Need mapped records |
| ✅ one/time | ❌ until ConsumeAsync | Large/lazy |
| ❌ discards | ✅ | Need counters |
| ✅ exactly 1 | ❌ | Expect 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游标消费方法:
| 方法 | 记录 | 统计信息 | 用途 |
|---|---|---|---|
| ✅ 全部 | ❌ | 需要所有记录 |
| ✅ 映射后 | ❌ | 需要映射后的记录 |
| ✅ 逐条/按需 | ❌ 直到调用ConsumeAsync | 大结果集/延迟加载 |
| ❌ 丢弃 | ✅ | 需要统计计数器 |
| ✅ 仅一条 | ❌ | 预期仅返回一行 |
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 default | Notes |
|---|---|---|
| | safe: |
| | safe: |
| | use |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | use nullable types |
ElementIdcsharp
// 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默认类型 | 说明 |
|---|---|---|
| | 兼容: |
| | 兼容: |
| | 可为空时使用 |
| | |
| | |
| | |
| | 包含 |
| | 包含 |
| | .NET 6+可调用 |
| | 可调用 |
| | |
| | 包含月/日时调用 |
| | 使用可空类型 |
ElementIdcsharp
// 传递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 for UNWIND do not serialize — use or .
WithParametersnew object[] { new { ... } }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();传递给用于UNWIND的自定义类实例无法序列化 — 使用或。
WithParametersnew 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 before — it's a subclass; generic handler swallows it.
ClientExceptionNeo4jExceptionex.GqlStatusex.CodeExplicit 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 throws a network error, commit may or may not have succeeded — design writes idempotent with + unique constraints.
CommitAsync()MERGEcsharp
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) { /* 其他所有服务器错误 */ }务必在之前捕获 — 它是子类,通用处理会覆盖它。
Neo4jExceptionClientExceptionex.GqlStatusex.Code显式事务回滚本身可能抛出异常 — 单独隔离处理:
csharp
catch (Exception original)
{
try { await tx.RollbackAsync(); }
catch (Exception ex) { logger.LogError(ex, "Rollback failed"); }
throw;
}如果抛出网络错误,提交可能成功也可能失败 — 使用+唯一约束设计幂等写操作。
CommitAsync()MERGECommon Mistakes
常见错误
| Mistake | Fix |
|---|---|
| |
| |
| Register as Singleton |
| Never — open per unit of work |
Missing | Task silently never runs |
| Remove |
Omit | Always specify — saves a round-trip |
No | Propagate |
| |
| Check |
| Last record, not null — don't use after loop |
| Throws — stop loop, don't call again |
| Return cursor from managed tx callback | Consume with |
| Need counters from session write | |
| Add |
| Use |
Custom class in | Use anonymous types or |
Rename C# param but not Cypher | Anonymous property names must match |
| Use |
| Side effects inside managed tx callback | Move outside — callback retried on failure |
| Only safe for pure second/nanosecond durations |
Catch | |
| 错误写法 | 修复方案 |
|---|---|
| |
| |
DI中 | 注册为Singleton |
DI中注册 | 绝不要 — 每个工作单元单独打开 |
| 任务会静默不执行 |
| 移除 |
| 务必指定 — 减少一次网络往返 |
Web应用中未使用 | 传递 |
对null图值调用 | 使用 |
| 先检查 |
FetchAsync循环后使用 | 保存的是最后一条记录而非null — 循环结束后不要使用 |
| FetchAsync()返回false后再次调用 | 会抛出异常 — 停止循环,不要再次调用 |
| 从托管事务回调返回游标 | 在回调内使用 |
| 会话写操作需要统计信息 | 使用 |
| 添加 |
使用 | 使用 |
UNWIND时 | 使用匿名类型或 |
修改C#参数名但未修改Cypher中的 | 匿名属性名必须与 |
读操作使用 | 使用 |
| 托管事务回调内有副作用 | 移到外部 — 回调在故障时会重试 |
包含月/日的 | 仅适用于纯秒/纳秒的Duration |
在 | |
References
参考文档
Load on demand:
- references/transactions.md — explicit transactions, , rollback, commit uncertainty,
BeginTransactionAsync(timeout, metadata), causal consistency and bookmarksTransactionConfig - references/performance.md — spatial types (Point/WGS-84/Cartesian), connection pool tuning, , session config options,
WithFetchSizepatterns, large result streamingCancellationToken - references/object-mapping.md — , blueprint mapping, lambda mapping,
AsObject<T>, repository pattern exampleAsObjectsAsync<T>
按需加载:
- references/transactions.md — 显式事务、、回滚、提交不确定性、
BeginTransactionAsync(超时、元数据)、因果一致性和书签TransactionConfig - references/performance.md — 空间类型(Point/WGS-84/Cartesian)、连接池调优、、会话配置选项、
WithFetchSize模式、大结果集流式处理CancellationToken - references/object-mapping.md — 、蓝图映射、Lambda映射、
AsObject<T>、仓储模式示例AsObjectsAsync<T>
Checklist
检查清单
- registered as singleton in DI (or
IDriverfor short-lived apps)await using - on driver and sessions (not plain
await using)using - specified in
database/QueryConfigconfigAsyncSession - used for simple queries;
ExecutableQuery/ExecuteReadAsyncfor streaming/multi-queryExecuteWriteAsync - Cursor consumed inside managed tx callback (not returned)
- Nullable types (,
string?) on any graph value that can be nullint? - used (no string interpolation)
WithParameters() - UNWIND batching with anonymous types (not custom class instances)
- propagated in web app handlers
CancellationToken - caught before
ClientExceptionNeo4jException - Writes idempotent (+ constraints) for retry safety
MERGE - No side effects inside /
ExecuteReadAsynccallbacksExecuteWriteAsync
- 在DI中注册为单例(短期应用使用
IDriver)await using - 驱动和会话使用(而非普通
await using)using - /
QueryConfig配置中指定了AsyncSessiondatabase - 简单查询使用;流式/多查询使用
ExecutableQuery/ExecuteReadAsyncExecuteWriteAsync - 托管事务回调内消费游标(不返回)
- 可能为null的图值使用可空类型(,
string?)int? - 使用(不使用字符串拼接)
WithParameters() - UNWIND批量处理使用匿名类型(而非自定义类实例)
- Web应用处理程序中传递
CancellationToken - 在之前捕获
Neo4jExceptionClientException - 写操作设计为幂等(+约束)以支持重试
MERGE - /
ExecuteReadAsync回调内无副作用ExecuteWriteAsync