后端与接口
用 C#/.NET 连接 Neo4j…
在 .NET/C# 中编写连接 Neo4j 的代码(Neo4j.Driver v6),涉及 IDriver、DI 注册、ExecutableQuery、托管事务、结果游标与类型映射时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
用 C#/.NET 连接 Neo4j 时特定
解决思路
- 安装包:`dotnet add package Neo4j.Driver`;异步 API 用 `Neo4j.Driver`,同步包装用 `Neo4j.Driver.Simple`,响应式流用 `Neo4j.Driver.Reactive`。
- `IDriver` 线程安全、带连接池、创建开销大,每个应用只创建一个。URI 方案:`neo4j+s://`(TLS + 集群路由,Aura)、`neo4j://`(非加密 + 集群路由)、`bolt+s://`(TLS + 单实例)、`bolt://`(非加密 + 单实例)。启动时调用 `await driver.VerifyConnectivityAsync()` 快速失败。
- `IDriver` 和 `IAsyncSession` 实现 `IAsyncDisposable`,必须用 `await using`,不要用普通 `using`(同步 Dispose 可能阻塞线程池)。
- 认证方式:`AuthTokens.Basic(u, p)` / `AuthTokens.Bearer(token)` / `AuthTokens.Kerberos(ticket)` / `AuthTokens.None`。
- 连接配置从环境变量或 `appsettings.json` 读取,绝不硬编码凭据;用 `builder.Configuration["Neo4j:Uri"]` 等访问,环境变量覆盖用双下划线 `Neo4j__Uri`;不要把含真实凭据的 `appsettings.json` 提交到仓库,改用 gitignored 的 `appsettings.Development.json` 或 CI/生产环境变量。
- 在 ASP.NET Core 中把 `IDriver` 注册为 singleton(绝不用 Scoped/Transient),绝不在 DI 中注册 `IAsyncSession`;用 `AddHostedService<Neo4jShutdownService>()` 在 `ApplicationStopping` 时调用 `driver.DisposeAsync()` 干净释放单例。
- 按场景选择 API:`driver.ExecutableQuery()` 适合大多数查询(自动重试、eager);`session.ExecuteReadAsync/WriteAsync()` 适合大结果集或多查询事务(自动重试、流式);`session.RunAsync()` 适合 `LOAD CSV`、`CALL {} IN TRANSACTIONS`(不重试、流式);`session.BeginTransactionAsync()` 适合跨函数、需外部协调的场景(不重试、流式)。
- 用 `ExecutableQuery` 流式构建器:`WithParameters(new { name = "Alice" })` 传参、`WithConfig(new QueryConfig(database: "neo4j", routing: RoutingControl.Readers))` 配置数据库与路由、`ExecuteAsync(cancellationToken)` 执行;用 `summary.ResultConsumedAfter` 测总墙钟时间(`ResultAvailableAfter` 只是首字节时间);写操作用 `writeSummary.Counters.NodesCreated` 取计数;用 `WithMap(r => r["name"].As<string>())` 内联投影。
- 绝不要漏写 `await`:`ExecuteAsync()` 返回 `Task`,漏写会静默编译通过但查询永不执行。绝不要字符串插值拼接 Cypher,始终用 `WithParameters()` 防注入并启用执行计划缓存。
- 托管事务用于大结果集(惰性流式)或一个事务内多条查询;回调在瞬时故障时自动重试,因此回调内必须幂等、不能有副作用。读用 `session.ExecuteReadAsync`(路由到副本),写用 `session.ExecuteWriteAsync`。
- 游标必须在回调内部消费:用 `ToListAsync()` 或 `FetchAsync()` 循环在回调内取完;把游标从回调返回会导致事务关闭、游标失效并抛异常。
- 避免 async void 陷阱:`ExecuteWriteAsync(async tx => tx.RunAsync(...))` 无内部 await 会产生 CS1998 警告且 Task 被丢弃;应去掉 `async` 直接返回 Task。
- `FetchAsync()` 循环:`while (await cursor.FetchAsync())` 为 true 时处理 `cursor.Current`;循环结束后不要再用 `cursor.Current`(它保存最后一条记录而非 null);`FetchAsync()` 返回 false 后不要再调用(会抛 InvalidOperationException)。
- 按需选择游标消费方法:`ToListAsync()` 取全部记录无 summary;`ToListAsync(mapper)` 取映射后记录;`FetchAsync()` 循环逐条惰性读取;`ConsumeAsync()` 丢弃记录返回 summary(用于取计数);`SingleAsync()` 期望恰好一行。
- `IResultSummary` 性能分析 [v6.3.0+]:`summary.Profile` 已 `[Obsolete]`,改用 `summary.QueryProfile`(`IQueryProfile`),其 `DbHits`、`Rows`、`Time`、`PageCacheHits`、`PageCacheMisses` 均为 `long?`,`null` 表示未记录(区别于 0);用 `summary.HasProfile` 判断。
- 记录取值:优先 `record.Get<string>("name")`,也可 `record["name"].As<string>()` 或按列索引 `record[0].As<string>()`;对可能为 null 的图值用可空类型 `record["city"].As<string?>()`(非可空会抛 InvalidCastException);键不存在会抛 KeyNotFoundException,先用 `record.Keys.Contains("city")` 检查。
- 类型映射:`Integer`→`long`、`Float`→`double`、`String`→`string`、`Boolean`→`bool`、`List`→`IReadOnlyList<object>`、`Map`→`IReadOnlyDictionary<string,object>`、`Node`→`INode`、`Relationship`→`IRelationship`、`Date`→`LocalDate`(`.ToDateOnly()`)、`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。
- 可直接把 CLR 类型作为参数传入,驱动会自动转换,例如 `WithParameters(new { ts = DateTimeOffset.UtcNow })`。
- UNWIND 批量写入:不要每条记录一个事务(开销高),改为 `UNWIND $rows AS row MERGE (n:Node {id: row.id}) SET n.name = row.name` 单事务处理;参数用匿名类型 `items.Select(i => new { id = i.Id, name = i.Name }).ToArray()` 或 `Dictionary<string, object>`,自定义类实例不会序列化。
- 对象映射(预览 API):必须 `using Neo4j.Driver.Preview.Mapping;`(否则 `AsObject<T>()` 报 CS1061);用 C# record 定义模型,`result.Result[0].AsObject<Person>()` 单条映射,`AsObjectsAsync<Person>()` 批量映射,RETURN 键名需对应 record 属性名。
- 错误处理:按顺序捕获 `AuthenticationException`(凭据错误)、`ServiceUnavailableException`(数据库不可达)、`ClientException`(如 `ex.Code == "Neo.ClientError.Schema.ConstraintValidationFailed"` 约束冲突,必须在 `Neo4jException` 之前捕获,因为它是子类)、`Neo4jException`(其他服务端错误)。优先用 `ex.GqlStatus` 稳定状态码而非字符串匹配 `ex.Code`。
- 显式事务回滚本身可能抛异常,需隔离:在 catch 中 `try { await tx.RollbackAsync(); } catch (Exception ex) { logger.LogError(ex, "Rollback failed"); } throw;`。
- 若 `CommitAsync()` 抛网络错误,提交可能成功也可能失败——用 `MERGE` + 唯一约束把写操作设计为幂等以支持重试。
- 避免常见错误:`using var driver`→`await using var driver`;`using var session`→`await using var session`;`IDriver` 不要注册为 Scoped/Transient;`IAsyncSession` 不要放进 DI;`ExecuteAsync()` 不要漏 await;`async tx => tx.RunAsync(...)` 无内部 await 时去掉 async;`QueryConfig`/`AsyncSession` 必须指定 `database`(省一次往返);Web 应用要传 `CancellationToken`(如 `HttpContext.RequestAborted`);null 图值用 `.As<string?>()`;`record["key"]` 前检查 `record.Keys.Contains()`;`FetchAsync` 循环后不要用 `cursor.Current`;`FetchAsync()` 返回 false 后不要再调用;不要从托管事务回调返回游标;需要计数时用 `await cursor.ConsumeAsync()`;`AsObject<T>()` 报 CS1061 时加 `using Neo4j.Driver.Preview.Mapping;`;总耗时用 `ResultConsumedAfter` 而非 `ResultAvailableAfter`;UNWIND 参数用匿名类型或 `Dictionary<string,object>`;C# 参数名要与 Cypher `$param` 名一致;读操作用 `ExecuteReadAsync` 而非 `ExecuteWriteAsync`;副作用移出托管事务回调;`Duration.ToTimeSpan()` 仅对纯秒/纳秒时长安全;先捕获 `ClientException` 再捕获 `Neo4jException`。
- 按需加载参考文档:`references/transactions.md`(显式事务、`BeginTransactionAsync`、回滚、提交不确定性、`TransactionConfig` 超时与元数据、因果一致性与书签)、`references/performance.md`(空间类型 Point/WGS-84/Cartesian、连接池调优、`WithFetchSize`、会话配置项、`CancellationToken` 模式、大结果流式)、`references/object-mapping.md`(`AsObject<T>`、blueprint 映射、lambda 映射、`AsObjectsAsync<T>`、仓储模式示例)。
- 完成前核对清单:`IDriver` 在 DI 中为 singleton(短生命周期应用用 `await using`);driver 和 session 用 `await using`;`QueryConfig`/`AsyncSession` 指定 `database`;简单查询用 `ExecutableQuery`,流式/多查询用 `ExecuteReadAsync`/`ExecuteWriteAsync`;游标在托管事务回调内消费;可能为 null 的图值用可空类型;用 `WithParameters()` 不做字符串插值;UNWIND 批量用匿名类型;Web 处理器传 `CancellationToken`;先捕获 `ClientException` 再捕获 `Neo4jException`;写操作幂等(`MERGE` + 约束);托管事务回调内无副作用。
适用边界 · 注意事项
- 编写或优化 Cypher 查询时——应使用 neo4j-cypher-skill。
- 从旧版驱动升级时——应使用 neo4j-migration-skill。