Map query results to objects

When reading data from the database, results come back as IRecord objects. To process records, you can either extract properties with .Get() and other helper methods, or you can map them to objects in your application. You can either map records to classes you have explicitly defined, or provide a record-specific blueprint from which the driver will spawn new objects.

The object mapping feature can help you reduce boilerplate code and work more easily with the results of your queries.

To use the object mapping feature, you need to include the namespace Neo4j.Driver.Mapping beside the root Neo4j.Driver.

Map to existing classes

If you want to map records to an existing class, define a class having the same attributes as the keys returned by the query. The class attributes must match exactly the query return keys, and they are case-sensitive.

To map a record to a class named className, use the method IRecord.AsObject<className>().

A record’s property names and its query return keys can be different.
For example, consider a node with label :Person and a name property.
The returned keys for the query MERGE (p:Person {name: "Alice"}) RETURN p.name are p.name, even if the property name is name.
Similarly, for the query MERGE (pers:Person {name: "Alice"}) RETURN pers.name, the return keys are pers.name.
You can always alter the return key with the Cypher operator AS (ex. MERGE (p:Person {name: "Alice"}) RETURN p.name AS name).
Map a :Person node to a Person class
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var record = result.Result[0];
var person = record.AsObject<Person>();  (2)
Console.WriteLine(person.name);  // "Alice"

class Person {  (3)
    public string name { get; set; }  (4)
    public int age { get; set; }
}
1 Return keys are aliased in the query to match the class attributes.
2 The record is mapped to an instance of the Person class.
3 The class Person contains name and age attributes, as the query return keys.
4 Each class attribute must have a public getter and setter, which the driver can invoke after instantiating the class. If that doesn’t fit your use-case, see how to use constructors and have private setters.

You can alter the association between property names and class attributes with the class attribute decorator [MappingSource("returnedKeyName")]. For more information, see all decorators.

Map p.name to name and p.age to age
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var person = result.Result[0].AsObject<Person>();
Console.WriteLine(person.name);  // "Alice"

class Person {  (2)
    [MappingSource("p.name")] (3)
    public string name { get; set; }

    [MappingSource("p.age")]
    public int age { get; set; }
}
1 The query returns the node, which results in the record keys p.name and p.age.
2 The class Person has attributes name and age, even if there’s no such return keys in the query.
3 The MappingSource decorator associates the record key p.name with the class attribute name.

Class attribute decorators

To make the mapping more flexible, decorators are available. Use them on class attributes to alter how each attribute should be mapped to its respective query return key.

  • [MappingSource("<returnedKeyName>")] — Map returnedKeyName to the class attribute, instead of looking for a returned key that matches the class attribute name.

  • [MappingIgnored] — Ignore the class attribute (i.e. don’t look for a corresponding returned key).

  • [MappingOptional] — Use the class attribute if it can be matched, but ignore it if the query doesn’t return a compatible key to match.

  • [MappingDefaultValue(val)] — Same as [MappingOptional], and specify a default value val in case of missing match.

Decorators example
class Person {
    [MappingSource("p.name")]  (1)
    public string name { get; set; }

    [MappingOptional]  (2)
    public int age { get; set; }

    [MappingIgnored]  (3)
    public string address { get; set; }

    [MappingDefaultValue("Subscriber")]  (4)
    public string role { get; set; }
}
1 Map the p.name return key to the name class attribute.
2 Populate the class attribute age with the age return key if present, and set the attribute to null otherwise.
3 Set the address class attribute to null regardless of the query return keys.
4 Populate the class attribute role with the role return key if present, and set the attribute to the default value Subscriber otherwise.

An extra optional parameter to the [MappingSource] decorator allows you to map node labels and relationship types into class attributes:

  • [MappingSource("nodeEntity", EntityMappingSource.NodeLabel)] — Collect the labels found in the node returned under the alias nodeEntity into the given class attribute as a list of strings. The first argument to MappingSource must be a returned key referencing a node entity. If, instead of List<string>, the attribute type is plain string, you obtain a comma-separated list of labels.

  • [MappingSource("relEntity", EntityMappingSource.RelationshipType)] — Map the relationship type job into the given class attribute. The first argument to [MappingSource] must be a returned key referencing a relationship entity.

Mapping node labels and relationship types
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person:Actor {name: $name})
    MERGE (m:Movie {name: $movie})
    MERGE (p)-[r:ACTED_IN {role: $role}]->(m)
    RETURN p AS person, r AS role  (1)
    ")
    .WithParameters(new { name = "Keanu Reeves", movie = "The Matrix", role = "Neo" })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var person = result.Result[0].AsObject<Person>();
Console.WriteLine(string.Join(",", person.Labels));  // "Person,Actor"
Console.WriteLine(person.Role);  // "Neo"

class Person {
    [MappingSource("person", EntityMappingSource.NodeLabel)]  (2)
    public List<string> Labels { get; set; }

    [MappingSource("role", EntityMappingSource.RelationshipType)]  (3)
    public string Role { get; set; }
}
1 person is a node entity; role is a relationship entity.
2 Labels collects the labels found in the person node.
3 Role contains the relationship type of the role relationship.

Work with constructors

When mapping records to an existing class, the first thing the driver has to do is to instantiate an object onto which to map records, which results in the class constructor to be invoked.

  • If no explicit constructor is defined, the driver chooses the implicitly-defined, trivial constructor and then proceeds to set the class attributes via their setters.

  • If one or more constructor are defined, the driver chooses the simplest one (i.e. the one with least parameters). You can override this choice by decorating with [MappingConstructor] the constructor you wish the driver to use.

Defining a constructor is useful in making the resulting objects immutable: the constructor provides the driver with a way to set some (or all) attributes, but no other component is allowed to change those attributes. After invoking the constructor, the driver proceeds to set values for attributes with public setters, if any.

You can use decorators in constructor arguments, although the only meaningful ones are [MappingOptional] and [MappingDefaultValue] (anything else is best achieved by changing the constructor signature).

A class with two constructors and private setters
class Person {
    [MappingConstructor]  (1)
    public Person(string name, [MappingDefaultValue(21)] int age) {
        Name = name;
        Age = age;
    }

    public Person() {  (2)
    }

    public string Name { get; private set; }  (3)
    public int Age { get; private set; }
}
1 The decorator instructs the driver to use this as constructor when mapping records onto the Person class. The constructor signature implies that the name return key is mapped to the class attribute Name, and age to Age (with a default value of 21).
2 This form of constructor would be invoked if the [MappingConstructor] decorator was not present in the other constructor.
3 Because the constructor sets the Name and Age attributes, the attributes can have private setters.

Map to anonymous types

Especially if your queries return a variety of result keys, it can feel pointless to define a number of different classes just for the mapping to work, or to engineer one single class and try to make it adapt to a variety of return keys with attribute decorators. In those cases, you can provide a skeleton of definition (a blueprint) to the driver and let it instantiate anonymous objects out of that.

You provide a blueprint with the method IRecord.AsObjectFromBlueprint(), and the record must match it exactly (i.e. you can’t use decorators).

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var person = result.Result[0].AsObjectFromBlueprint(new { name = "", age = 0 });  (2)
1 Return keys must be aliased, as .AsObjectFromBlueprint() doesn’t support dots in identifiers.
2 Key names must match the return keys. Provided values don’t work as default values; they are only used to infer the property type (i.e. the specific values provided in the definition are discarded).

If you want to avoid aliases in return keys, you can nest objects in the .AsObjectFromBlueprint() call.

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p  (1)
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var person = result.Result[0].AsObjectFromBlueprint(new { p = new { name = "", age = 0 }});  (2)
1 The only return key is p, containing p.name and p.age.
2 A nested object blueprint handles the node entity return key.

An alternative way of mapping to anonymous types is to use .AsObject() with a lambda function. That allows you to also add attributes that don’t match a property from the database (such as BirthYear in the example below).

var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var person = result.Result[0].AsObject( (string name, int age) => new { Name = name, Age = age, BirthYear = 2025 - age });

The lambda function doesn’t have to create a map: it can do any processing.

A lambda returning a string
var result = await driver.ExecutableQuery(@"
    MERGE (p:Person {name: $name, age: $age})
    RETURN p.name AS name, p.age AS age
    ")
    .WithParameters(new { name = "Alice", age = 21 })
    .WithConfig(new QueryConfig(database: "neo4j"))
    .ExecuteAsync();

var text = result.Result[0].AsObject( (string name, int age) => $"{name} is {age} years old." );

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.