Query the database
Once you have connected to the database, you can execute Cypher queries through the method IDriver.ExecutableQuery()
.
IDriver.ExecutableQuery() was introduced with the version 5.8 of the driver.For queries with earlier versions, use sessions and transactions. |
Write to the database
To create two node representing persons named Alice
and David
, and a relationship KNOWS
between them, use the Cypher clause CREATE
:
var result = await driver.ExecutableQuery(@" (1)
CREATE (a:Person {name: $name})
CREATE (b:Person {name: $friendName})
CREATE (a)-[:KNOWS]->(b)
")
.WithParameters(new { name = "Alice", friendName = "David" }) (2)
.WithConfig(new QueryConfig(database: "neo4j")) (3)
.ExecuteAsync();
var summary = result.Summary; (4)
Console.WriteLine($"Created {summary.Counters.NodesCreated} nodes in {summary.ResultAvailableAfter.Milliseconds} ms.");
1 | The Cypher query |
2 | A map of query parameters |
3 | The database to run the query on |
4 | The summary of execution returned by the server |
Read from the database
To retrieve information from the database, use the Cypher clause MATCH
:
Person
nodes who like other Person
svar result = await driver.ExecutableQuery(@"
MATCH (p:Person)-[:KNOWS]->(:Person)
RETURN p.name AS name
")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
// Loop through results and print people's name
foreach (var record in result.Result) { (1)
Console.WriteLine(record.Get<string>("name")); (2)
}
// Summary information
var summary = result.Summary; (3)
Console.WriteLine($"The query `{summary.Query.Text}` returned {result.Result.Count()} results in {summary.ResultAvailableAfter.Milliseconds} ms.");
1 | result.Result contains the result as a list of IRecord objects |
2 | .Get<type>(key) extracts the entry named key from the returned record and casts it to type .
For more information on types, see Data types and mapping to Cypher types. |
3 | The summary of execution returned by the server |
To clean up the returned data structure, use the .WithMap()
method when running the query.
This can be especially handy when returning nodes rather than individual properties.
For example,
var result = await driver.ExecutableQuery(@"
MATCH (p:Person)-[:KNOWS]->(:Person)
RETURN p.name AS name
")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithMap(record => record["name"].As<string>())
.ExecuteAsync();
foreach (var name in result.Result) {
Console.WriteLine(name);
}
An alternative way of processing records is to map them to objects.
Update the database
Alice
to add an age
propertyvar result = await driver.ExecutableQuery(@"
MATCH (p:Person {name: $name})
SET p.age = $age
")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { name = "Alice", age = 42 })
.ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");
To create a new relationship, linking it to two already existing nodes, use a combination of the Cypher clauses MATCH
and CREATE
:
:KNOWS
between Alice
and Bob
var result = await driver.ExecutableQuery(@"
MATCH (alice:Person {name: $name}) (1)
MATCH (bob:Person {name: $friend}) (2)
CREATE (alice)-[:KNOWS]->(bob) (3)
")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { name = "Alice", friend = "Bob" })
.ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");
1 | Retrieve the person node named Alice and bind it to a variable alice |
2 | Retrieve the person node named Bob and bind it to a variable bob |
3 | Create a new :KNOWS relationship outgoing from the node bound to alice and attach to it the Person node named Bob |
Delete from the database
To remove a node and any relationship attached to it, use the Cypher clause DETACH DELETE
:
Alice
node and all its relationships// This does not delete _only_ `p`, but also all its relationships!
var result = await driver.ExecutableQuery(@"
MATCH (p:Person {name: $name})
DETACH DELETE p
")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { name = "Alice" })
.ExecuteAsync();
Console.WriteLine($"Query updated the database? {result.Summary.Counters.ContainsUpdates}");
Query parameters
Do not hardcode or concatenate parameters directly into queries. Instead, always use placeholders and specify the Cypher parameters, as shown in the previous examples. This is for:
-
performance benefits: Neo4j compiles and caches queries, but can only do so if the query structure is unchanged;
-
security reasons: see protecting against Cypher injection.
You can provide query parameters as a map through the .WithParameters()
method.
await driver.ExecutableQuery("MATCH (p:Person {name: $name}) RETURN p")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { name = "Alice" })
.ExecuteAsync();
There can be circumstances where your query structure prevents the usage of parameters in all its parts. For those rare use cases, see Dynamic values in property keys, relationship types, and labels. |
Error handling
A query run may fail for a number of reasons.
When using IDriver.ExecutableQuery()
, the driver automatically retries to run a failed query if the failure is deemed to be transient (for example due to temporary server unavailability).
An error will be raised if the operation keeps failing after the configured maximum retry time.
All errors coming from the server are of type Neo4jException
.
You can use an error’s code to stably identify a specific error; error messages are instead not stable markers, and should not be relied upon.
try {
await driver.ExecutableQuery("MATCH (p:Person) RETURN")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
} catch (Neo4jException e) {
Console.WriteLine($"Neo4j error code: {e.Code}");
Console.WriteLine($"Exception message: {e.Message}");
}
/*
Neo4j error code: Neo.ClientError.Statement.SyntaxError
Exception message: Invalid input '': expected an expression, '*', 'ALL' or 'DISTINCT' (line 1, column 24 (offset: 23))
"MATCH (p:Person) RETURN"
*/
Query configuration
You can supply further configuration parameters to alter the default behavior of .ExecutableQuery()
.
You do so through the method .WithConfig()
, which takes a QueryConfig
object.
Database selection
It is recommended to always specify the database explicitly, even on single-database instances. This allows the driver to work more efficiently, as it saves a network round-trip to the server to resolve the home database. If no database is given, the user’s home database is used.
await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
.WithConfig(new QueryConfig(database: "neo4j"))
.ExecuteAsync();
Specifying the database through the configuration method is preferred over the USE Cypher clause.
If the server runs on a cluster, queries with USE require server-side routing to be enabled.
Queries may also take longer to execute as they may not reach the right cluster member at the first attempt, and need to be routed to one containing the requested database.
|
Request routing
In a cluster environment, all queries are directed to the leader node by default.
To improve performance on read queries, use routing: RoutingControl.Readers
to route a query to the read nodes.
await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
.WithConfig(new QueryConfig(
database: "neo4j",
routing: RoutingControl.Readers
))
.ExecuteAsync();
Although executing a write query in read mode likely results in a runtime error, you should not rely on this for access control. The difference between the two modes is that read transactions will be routed to any node of a cluster, whereas write ones are directed to primaries. There is no guarantee that a write query submitted in read mode will be rejected, and an issue with this feature will not be classified as a security vulnerability. |
Run queries as a different user
You can execute a query through a different user with the config key authToken
.
Switching user at the query level is cheaper than creating a new IDriver
object.
The query is then run within the security context of the given user (i.e., home database, permissions, etc.).
See AuthTokens
for available native implementations of authentication methods.
Query-scoped authentication requires a Neo4j server version >= 5.8.
await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
.WithConfig(new QueryConfig(
database: "neo4j",
authToken: AuthTokens.Basic("somebodyElse", "theirPassword")
))
.ExecuteAsync();
The QueryConfig
key impersonatedUser
provides a similar functionality, and is available in driver/server versions >= 4.4.
The difference is that you don’t need to know a user’s password to impersonate them, but the user under which the IDriver
was created needs to have IMPERSONATE
privileges.
await driver.ExecutableQuery("MATCH (p:Person) RETURN p.name")
.WithConfig(new QueryConfig(
database: "neo4j",
impersonatedUser: "somebodyElse"
))
.ExecuteAsync();
A full example
This example uses the Cypher clause MERGE
to avoid data duplication in case the code is run more than once.
using Neo4j.Driver;
// URI examples: "neo4j://localhost", "neo4j+s://xxx.databases.neo4j.io"
const string dbUri = "<database-uri>";
const string dbUser = "<username>";
const string dbPassword = "<password>";
await using var driver = GraphDatabase.Driver(dbUri, AuthTokens.Basic(dbUser, dbPassword));
await driver.VerifyConnectivityAsync();
Console.WriteLine("Connection established.");
// Toy dataset
var people = new List<Dictionary<string, dynamic>>();
people.Add(
new Dictionary<string, dynamic>() {
{"name", "Alice"},
{"age", 42},
{"friends", new List<string>(){"Bob", "Peter", "Anna"}},
}
);
people.Add(
new Dictionary<string, dynamic>() {
{"name", "Bob"},
{"age", 19},
}
);
people.Add(
new Dictionary<string, dynamic>() {
{"name", "Peter"},
{"age", 50},
}
);
people.Add(
new Dictionary<string, dynamic>() {
{"name", "Anna"},
{"age", 33},
}
);
try {
//Create some nodes
foreach (var person in people) {
await driver
.ExecutableQuery("MERGE (:Person {name: $person.name, age: $person.age})")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { person = person })
.ExecuteAsync();
}
// Create some relationships
foreach (var person in people) {
if (person.ContainsKey("friends")) {
await driver.ExecutableQuery(@"
MATCH (p:Person {name: $person.name})
UNWIND $person.friends AS friend_name
MATCH (friend:Person {name: friend_name})
MERGE (p)-[:KNOWS]->(friend)
")
.WithConfig(new QueryConfig(database: "neo4j"))
.WithParameters(new { person = person })
.ExecuteAsync();
}
}
// Retrieve Alice's friends who are under 40
Console.WriteLine("Alice's friends under 40:");
var result = await driver.ExecutableQuery(@"
MATCH (p:Person {name: $name})-[:KNOWS]-(friend:Person)
WHERE friend.age < $age
RETURN friend
")
.WithConfig(new QueryConfig(database: "neo4j", routing: RoutingControl.Readers))
.WithParameters(new { name = "Alice", age = 40 })
.ExecuteAsync();
// Loop through results and do something with them
foreach (var record in result.Result) {
Console.WriteLine(record.Get<INode>("friend").Get<string>("name"));
}
// Summary information
var summary = result.Summary;
Console.WriteLine($"The query `{summary.Query.Text}` returned {result.Result.Count()} results in {summary.ResultAvailableAfter.Milliseconds} ms.");
} catch (Neo4jException e) {
Console.WriteLine(e);
Environment.Exit(1);
}
For more information see API documentation → IDriver.ExecutableQuery().
Glossary
- LTS
-
A Long Term Support release is one guaranteed to be supported for a number of years. Neo4j 4.4 is LTS, and Neo4j 5 will also have an LTS version.
- Aura
-
Aura is Neo4j’s fully managed cloud service. It comes with both free and paid plans.
- Cypher
-
Cypher is Neo4j’s graph query language that lets you retrieve data from the database. It is like SQL, but for graphs.
- APOC
-
Awesome Procedures On Cypher (APOC) is a library of (many) functions that can not be easily expressed in Cypher itself.
- Bolt
-
Bolt is the protocol used for interaction between Neo4j instances and drivers. It listens on port 7687 by default.
- ACID
-
Atomicity, Consistency, Isolation, Durability (ACID) are properties guaranteeing that database transactions are processed reliably. An ACID-compliant DBMS ensures that the data in the database remains accurate and consistent despite failures.
- eventual consistency
-
A database is eventually consistent if it provides the guarantee that all cluster members will, at some point in time, store the latest version of the data.
- causal consistency
-
A database is causally consistent if read and write queries are seen by every member of the cluster in the same order. This is stronger than eventual consistency.
- NULL
-
The null marker is not a type but a placeholder for absence of value. For more information, see Cypher → Working with
null
. - transaction
-
A transaction is a unit of work that is either committed in its entirety or rolled back on failure. An example is a bank transfer: it involves multiple steps, but they must all succeed or be reverted, to avoid money being subtracted from one account but not added to the other.
- backpressure
-
Backpressure is a force opposing the flow of data. It ensures that the client is not being overwhelmed by data faster than it can handle.
- transaction function
-
A transaction function is a callback executed by an
.ExecuteReadAsync()
or.ExecuteWriteAsync()
call. The driver automatically re-executes the callback in case of server failure. - IDriver
-
A
IDriver
object holds the details required to establish connections with a Neo4j database.