Skip to content
Jesse Franceschini edited this page Feb 23, 2023 · 11 revisions

Performing Cypher Queries

Cypher support in Neo4jClient is relatively mature, but still undergoing active development to make it even better.

The intention of the Cypher support in Neo4jClient is to provide a nice syntax for constructing queries. You still need to know and understand Cypher. If you're not already familiar with Cypher, you should probably start by reading the documentation for it.

This page only describes the specific things you should know about in the context of the Neo4jClient driver. We recommend reading the whole page; we've tried to keep it short.

Starting a Query

Assuming a Book class:

public class Book
{
    public string Title { get; set; }
    public int Pages { get; set; }
}

A Cypher query can be started from the Cypher property of IGraphClient:

var query = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Return<Book>("book");

var longBooks = query.Results;

After you've called .Cypher, everything else is pretty much a one-to-one match with Cypher itself. Match will write a MATCH clause, etc.

For other approaches, please see the SO article: http://stackoverflow.com/questions/30314696/return-overload-fails

Returns

Neo4jclient exposes several flexible ways to return data. Assuming the same Book class as above, consider the Cypher statement:

RETURN book

The simplest implementation of this return statement contains just a type and a Cypher identifier:

.Return<Book>("book");

The results from neo4j are deserialized into an IEnumerable<> of the specified type. Results are accessed via the Results property of your Cypher query:

var query = client
    .Match(...)
    .Return(...);

var matches = query.Results;

More complicated return statements are built using lambda expressions. When using lambda expressions, the name of the lambda variable is passed through to the Cypher query, so your variable name must match your Cypher identifier. The following call yields identical results to the simple return shown above:

.Return(book => book.As<Book>());

If you need to return data that is not defined by a Cypher identifier, you can write your own return statement text using the Return static class. (Return is part of the Neo4jClient.Cypher namespace, so include using Neo4jClient.Cypher; to access Return). Once again, this return statement yields identical Cypher text to those above:

.Return(() => Return.As<Book>("book"));

A more reasonable use for this functionality is returning a property:

.Return(() => Return.As<long>("book.Pages"));

Without Anonymous Types

Some languages, like F#, don't support anonymous types. In these cases, you can supply a dictionary of start bits instead of an object. (If you look at the Neo4jClient code, when somebody supplies an object, we just convert it to a dictionary and then follow the other code path anyway.)

Identities in Lambda Expressions

Some of our methods take lambda expressions, like:

.Where((Book bk) => bk.Pages > 5)

In these scenarios, the argument names in the lambda are important because we flow them through to the resulting Cypher:

WHERE bk.Pages > 5

Returning Projections

In the example so far, we've only been returning one identity from the query:

.Return(book => book.As<Book>())

That results in this piece of Cypher text:

RETURN book

And gets deserialized into IEnumerable<Book>.

Let's extend this to return the publisher for each book:

var query = client.Cypher
    .Match("(book:Book)-[:PUBLISHED_BY]->(publisher:Publisher)")
    .Return((book, publisher) => new {
        Book = book.As<Book>(),
        Publisher = publisher.As<Publisher>(),
    });

We've extended the Match call and changed the Return call to use a lambda expression that creates an anonymous type.

That results in this piece of Cypher text:

RETURN book AS Book, publisher AS Publisher

As described in the previous section, the identities book and publisher came from the names of the arguments we used for the lambda expression.

We can now work with the results like so:

foreach (var result in query.Results)
{
    Console.WriteLine(result.Book.Title + " is published by " + result.Publisher.Name);
}

Using Functions in Return Clauses

Let's continue to extend our book example, this time by returning the authors for each book. This may be multiple author nodes per book, so we'll want to use Cypher's collect function.

These functions are available on the ICypherResultItem instance supplied as an argument to the lambda:

var query = client.Cypher
    .Match(
        "(book:Book)-[:PUBLISHED_BY]->(publisher:Publisher)",
        "(book)-[:WRITTEN_BY]->(author:Author)")
    .Return((book, publisher, author) => new {
        Book = book.As<Book>(),
        Publisher = publisher.As<Publisher>(),
        Authors = author.CollectAs<Author>()
    });

That will result in a Cypher RETURN clause of:

RETURN book as Book, publisher AS Publisher, collect(author) as Authors

The Authors property on the result type will be an IEnumerable<Author>:

foreach (var result in query.Results)
{
    Console.WriteLine(result.Book.Title + " is published by " + result.Publisher.Name);
    Console.WriteLine("It has " + result.Authors.Count() + " authors");
}

All Results

For functions that apply to all nodes in the set, use the special All class:

var query = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Return(() => All.Count());

That will result in a Cypher RETURN clause of:

RETURN count(*)

Wrapping Functions

To wrap functions, you can chain them:

.Return(books => new { BestBook = books.Head().CollectAs<Book>() });

That will result in a Cypher RETURN clause of:

RETURN head(collect(books)) AS BestBook

Using Custom Text in Return Clauses

It's not practical (or that useful) for us to try and model every last Cypher concept in C#. For the cases that we don't handle, you can always supply custom Cypher text:

.Return(() => new {
    YearsOfAuthorAgePerPage = Return.As<int>("round(avg(author.Age) / book.Pages)")
})

That will result in a Cypher RETURN clause of:

RETURN round(avg(author.Age) / book.Pages) AS YearsOfAuthorAgePerPage

You can combine this with your other return statements too:

.Return((book, author) => new {
    Book = book.As<Book>(),
    YearsOfAuthorAgePerPage = Return.As<int>("round(avg(author.Age) / book.Pages)")
})

That will result in a Cypher RETURN clause of:

RETURN book as Book, round(avg(author.Age) / book.Pages) as YearsOfAuthorAgePerPage

Lazy Enumerables

The query is only sent across the wire when you actually enumerate Results, or call ExecuteWithoutResults(). Until then, we don't know if you're still adding steps to the fluent query.

For example, this won't execute anything:

var query = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Delete("bk");

You need to explicitly execute it:

var query = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Delete("bk")
    .ExecuteWithoutResults();

This because even though you've added a DELETE clause, you might still want to add a RETURN clause yet. We have no way of knowing.

Multiple Enumerations

Every time you enumerate Results or call ExecuteWithoutResults, we execute the query against Neo4j.

This code will hit Neo4j twice, because Results is enumerated twice, even though it only calls the property once:

var books = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Return(book => book.As<Book>())
    .Results;

foreach (var book in books) { Console.WriteLine("A"); }
foreach (var book in books) { Console.WriteLine("B"); }

If you don't want that to happen, store .Results.ToList() in a variable instead and use that.

Immutability

Each of the objects constructed by the fluent query are immutable. This design contract allows you to conduct base queries, then effectively fork them.

var allBooks = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)");

var longBooks = allBooks
    .Where((Book bk) => bk.Pages > 5);

You can continue to build these queries independently until you execute them.

Parameters

Cypher parameters are the safe way to inject dynamic information into queries. They avoid the risk of injection based attacks, and ensure that your values are accurately encoded.

They also significantly improve query plan caching on the Neo4j side, because the query text doesn't change so Neo4j doesn't have to recompile the plan on every hit.

Explicit Parameters

You can create parameters at any point in the fluent query using WithParam. Order is unimportant because these are added to the parameters dictionary, not written into the query text.

Then, use your parameter in Cypher text: Clause("…{SomeParam}…").

For example:

.Clause("…{SomeParam}…")
.WithParam("SomeParam", 456)

Automatic Parameters

Wherever possible, we also leverage Cypher parameters automatically.

This means that a fluent query like this:

var booksQuery = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Return(book => book.As<Book>());

Results in a JSON payload like this:

{
    "query": "MATCH (:Shop)-[:HAS_BOOK]->(book:Book) WHERE bk.Pages > {p1} RETURN book",
    "parameters": {
        "p0": 0,
        "p1": 5
    }
}

That is, we've extracted the components of the query which are not compiled into the resulting Cypher query plan and pushed them into the parameters dictionary.

This all happens transparently to you.

The only place you'll notice is if you try to debug the raw query text. There are some tips and tricks around this that are described in the Debugging section later in this document.

Debugging

If you construct a fluent query:

var booksQuery = client.Cypher
    .Match("(:Shop)-[:HAS_BOOK]->(book:Book)")
    .Where((Book bk) => bk.Pages > 5)
    .Return(book => book.As<Book>());

You can then access the query text and parameters:

booksQuery.Query.QueryText
booksQuery.Query.QueryParameters

Because the query text will use parameter references, it will look like WHERE bk.Pages > $p0 and you won't be able to copy-paste it straight into a Neo4j console.

To make this easier, there's also a property called DebugQueryText which attempts to remove the parameter references and give you back a string like WHERE bk.Pages > 5 instead. We say "attempts" because it does this through some rather dumb string replace operations, so it's quite possible that the resulting query text will be slightly different from a true parameterless representation of the query. For example, we do absolutely nothing to try and escape the values correctly, because we don't have this logic anywhere else. (This is another pro of us just deferring parameter values to a JSON hash.) If you care about debugging exactly what is being sent to the server, you need to use QueryText and QueryParameters. If you want something that's usually good enough to help fix a basic query blunder, then DebugQueryText might be useful.

Manual Queries (highly discouraged)

If something in the fluent query builder is blocking you from executing the query you want, you can fall back to constructing and executing the query manually. You will still benefit from our JSON deserializer work, the same HTTP client, etc, but you'll have to supply the query as a string and parameter dictionary.

You will likely introduce a runtime security risk if you use this, by nature of constructing your own query string.

The direct access methods are considered internal and may be removed or renamed at any time.

You should not do this until you have exhausted all other options, which includes raising an issue so that we can remove the impediment.

This mechanism is highly discouraged unless you have a legitimate reason to require it. To discourage use, it's hidden behind an explicit interface implementation and sliced off from IGraphClient. That's how much we want to hide it.

Here are the magic methods though:

((IRawGraphClient)client).ExecuteCypher(query)

((IRawGraphClient)client).ExecuteGetCypherResults(query)

((IRawGraphClient)client).ExecuteGetCypherResultsAsync(query)

Please don't shoot yourself in the foot.