mcpField
N
neo4j-driver-dotnet-skill

neo4j-driver-dotnet-skill

github.com/neo4j-contrib开发118771 次安装1 周前更新
代码仓库

关于 neo4j-driver-dotnet-skill

面向编写 Neo4j .NET 应用的开发者,提供 Neo4j.Driver 使用指导,涵盖异步生命周期、查询执行、事务、结果映射与错误处理。内容侧重常见陷阱及安全、可靠的驱动实践。

主要功能

  1. 01IDriver 与会话生命周期指导
  2. 02查询 API 与事务选择
  3. 03异步游标和结果处理
  4. 04Cypher 参数与批处理建议
  5. 05类型映射和错误处理

适用场景

  1. 01在 ASP.NET Core 中注册和配置驱动
  2. 02实现异步查询及事务逻辑
  3. 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.md
references/
object-mapping.md
performance.md
transactions.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.ReactiveSystem.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 默认类型备注
Integerlong安全类型:int、long?、int?
Floatdouble安全类型:float、double?
Stringstring可空时使用 string?
Booleanbool
ListIReadOnlyList<object>
MapIReadOnlyDictionary<string,object>
NodeINode.Labels、.Properties、.ElementId
RelationshipIRelationship.Type、.StartNodeElementId
DateLocalDate.ToDateOnly()(.NET 6+)
DateTimeZonedDateTime.ToDateTimeOffset()(毫秒精度)
LocalDateTimeLocalDateTime
DurationDuration若包含月或天,.ToTimeSpan() 会抛出异常
UUIDSystem.Guid[driver 6.2+、Bolt 6.1、Neo4j 2026.08+];对象映射也支持与 string 相互转换
nullnull使用可空类型

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 driverawait using var driver——IDriver 实现了 IAsyncDisposable
using var sessionawait using var session
在 DI 中将 IDriver 注册为 Scoped/Transient注册为 Singleton
在 DI 中使用 IAsyncSession绝不要这样做——每个工作单元单独打开
ExecuteAsync() 缺少 awaitTask 会被静默地忽略
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,再捕获 ClientExceptionClientException 是其子类——应先捕获它

参考

按需加载:

  • 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 回调中执行副作用

评论

支持 Markdown