mcpField
N
neo4j-driver-dotnet-skill

neo4j-driver-dotnet-skill

github.com/neo4j-contrib開発118771 回インストール1 週間前に更新
リポジトリ

neo4j-driver-dotnet-skill について

Neo4j.Driver を使って .NET アプリケーションを開発する人向けのスキルです。非同期のライフサイクル、クエリとトランザクション、結果マッピング、よくある安全性・信頼性の問題を扱います。

主な機能

  1. 01IDriver とセッションのライフサイクル指針
  2. 02クエリ API とトランザクションの選択
  3. 03非同期カーソルと結果の処理
  4. 04Cypher パラメーターとバッチ処理の助言
  5. 05型マッピングとエラー処理

活用シーン

  1. 01ASP.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 でセッション、型マッピング、null 安全性、エラーハンドリングをデバッグする場合

使用しない場面

  • Cypher クエリの作成/最適化 → neo4j-cypher-skill
  • 古いドライバーのバージョンからアップグレード → neo4j-migration-skill

インストール

dotnet add package Neo4j.Driver
パッケージ用途
Neo4j.Driver非同期 API — こちらを使用してください
Neo4j.Driver.Simple同期ラッパー
Neo4j.Driver.ReactiveSystem.Reactive ストリーム

ドライバーのライフサイクル

IDriver はスレッドセーフで、接続プールを備え、作成コストが高いものです。アプリケーションごとに 1 つ作成してください。

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 にしてはいけません。IAsyncSession は DI に登録しないでください。

// 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() ループ✅ 1 件ずつ❌ ConsumeAsync まで大規模/遅延処理
ConsumeAsync()❌ 破棄✅カウンターが必要な場合

IResultSummary のプロファイリング[v6.3.0+]:summary.Profile は [Obsolete] のため、summary.QueryProfile(IQueryProfile)を使用してください。DbHits、Rows、Time、PageCacheHits、PageCacheMisses は long? です。null は記録されていないことを意味し、0 とは異なります。summary.HasProfile で確認してください。 | SingleAsync() | ✅ ちょうど 1 件 | ❌ | 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?
Stringstringnull 許容の場合は 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 との相互変換にも対応
nullnullnull 許容型を使用

ElementId は 1 つのトランザクション内でのみ安定します。別のトランザクション間での 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() に await を付け忘れるタスクが実行されないままになります
async tx => tx.RunAsync(...) で内部に await がないasync を削除し、Task を直接返す
QueryConfig/AsyncSession で database を省略する必ず指定する — 往復通信を 1 回分節約できます
Web アプリで CancellationToken を指定しないHttpContext.RequestAborted を伝播させる
null のグラフ値に .As<string>() を使う.As<string?>() を使う — null 非許容型では例外がスローされます
record["key"] のキーが存在しないまず record.Keys.Contains() で確認する
FetchAsync ループ後の cursor.Currentnull ではなく最後のレコードです — ループ後に使用しないでください
false が返された後に FetchAsync() を呼び出す例外がスローされます — ループを終了し、再度呼び出さないでください
管理対象トランザクションのコールバックからカーソルを返すコールバック内で 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()純粋な秒/ナノ秒の期間に限り安全です
ClientException より先に Neo4jException をキャッチするClientException はサブクラスなので、先にキャッチする

参考資料

必要に応じて読み込んでください:

  • references/transactions.md — 明示的なトランザクション、BeginTransactionAsync、ロールバック、コミットの不確実性、TransactionConfig(タイムアウト、メタデータ)、因果整合性とブックマーク
  • references/performance.md — 空間型(Point/WGS-84/Cartesian)、接続プールの調整、WithFetchSize、セッション設定オプション、CancellationToken のパターン、大きな結果セットのストリーミング
  • references/object-mapping.md — AsObject<T>、ブループリントマッピング、ラムダマッピング、AsObjectsAsync<T>、リポジトリパターンの例

チェックリスト

  • IDriver を DI でシングルトンとして登録する(または短時間だけ動作するアプリでは await using を使用する)
  • ドライバーとセッションには await using を使用する(通常の using ではなく)
  • QueryConfig/AsyncSession の設定で database を指定する
  • シンプルなクエリには ExecutableQuery を使用し、ストリーミング/複数クエリには ExecuteReadAsync/ExecuteWriteAsync を使用する
  • 管理対象トランザクションのコールバック内でカーソルを消費する(返さない)
  • null になり得るグラフ値には nullable 型(string?、int?)を使用する
  • WithParameters() を使用する(文字列補間は使用しない)
  • UNWIND バッチ処理では匿名型を使用する(カスタムクラスのインスタンスは使用しない)
  • Web アプリのハンドラーで CancellationToken を伝播する
  • Neo4jException より先に ClientException を捕捉する
  • 再試行時の安全性のため、書き込みを冪等にする(MERGE +制約)
  • ExecuteReadAsync/ExecuteWriteAsync のコールバック内で副作用を発生させない

コメント