At this point, we should take a look at the Cypher type system. As Neo4j is written in Java (the j in Neo4j stands for Java after all), there are some discrepancies between the types stored in the Neo4j database and native C# types.
Some values like strings, numbers, booleans, dates, and nulls map directly to C# types but more complex types like nodes, relationship, points, durations need special handling.
.NET Types to Neo4j/Cypher Types
Neo4j Cypher Type | .NET Type | Notes |
---|---|---|
null |
null |
|
List |
IList<object> |
|
Map |
IDictionary<string, object> |
|
Boolean |
|
|
Integer |
|
|
Float |
|
|
String |
|
|
ByteArray |
|
|
Date |
|
See Temporal Types |
Time |
|
See Temporal Types |
LocalTime |
|
See Temporal Types |
DateTime* |
|
See Temporal Types |
LocalDateTime |
|
See Temporal Types |
Duration |
|
|
Point |
|
|
Node |
|
|
Relationship |
|
|
Path |
|
-
Time zone names adhere to the IANA system, rather than the Windows system. Inbound conversion is carried out using Extended Windows-Olson zid mapping as defined by Unicode CLDR.
You can use the .As<Type>
method on any Values
type or nested structure to cast underlying values to expected types.
See the API docs for IRecord.Values
for more information.
For example, in the code block below, the year
value is cast as a Number
.
int movieCount = row["movieCount"].As<int>();
Let’s take a look at some of these types in more detail.
Numbers
For numeric values the main confusion comes from the naming, while Neo4j itself can store all kinds of .NET primitive values for a unified surface, Cypher only exposes one floating point type called Float
(equivalent to 64-bit double) and one integer type called Integer
(equivalent to 64-bit long).
Cypher itself has functions like toFloat()
and toInteger()
respectively and driver parameters of other types are automatically coerced.
The driver’s Value
type can return most .NET numeric types via .As<Type>
methods.
Temporal Types
The Temporal types used in the Cypher type system mirror the .NET temporal types, so there are few surprises.
Type | Description | Example | Access Function |
---|---|---|---|
|
Represents an instant capturing the date, but not the time, nor the timezone. |
|
|
|
Represents an instant capturing the date, the time and the timezone identifier. |
|
|
|
Represents an instant capturing the date and the time, but not the timezone. |
|
|
|
Represents an instant capturing the time of day, but not the date, nor the timezone. |
|
|
|
Represents a duration between two dates or timestamps (different resolutions). Has individual accessors for the parts. |
|
|
var released = node["released"];
var localDate = released.As<LocalDate>();
var localDateTime = released.As<LocalDateTime>();
var localTime = released.As<LocalTime>();
var dateTimeOffset = released.As<DateTimeOffset>();
Nodes & Relationships
Nodes and Relationships are both returned as similar types with a common interface IEntity
.
As an example, let’s take the following code snippet:
var cursor = await tx.RunAsync(@"
MATCH path = (person:Person)-[actedIn:ACTED_IN]->(movie:Movie)
RETURN path, person, actedIn, movie,
size((person)-[:ACTED]->()) as movieCount,
exists { (person)-[:DIRECTED]->() } as isDirector
LIMIT 1
");
Nodes
We can retrieve the person
value using the .Values["person"]
method on the row and then turn it into a INode
using as<INode>
.
// Get a node
INode person = row["person"].As<INode>();
The value assigned to the person
variable will be the instance of an INode
implementation.
INode
is an implementation provided by the Neo4j .NET Driver to represent the information held in Neo4j for a node.
An instance of a Node
has three parts:
long nodeId = person.Id; // (1)
IReadOnlyList<string> labels = person.Labels; // (2)
IReadOnlyDictionary<string, object> properties = person.Properties; // (3)
-
Id
- along
representing the internal ID for the node. -
Labels
- a list of string values, eg.['Person', 'Actor']
-
Properties
- A dictionary containing all the properties for the node.
eg.{"name": "Tom Hanks", "tmdbId": "31" }
Properties can also be retrieved from IEntity
instances using square brackets, for example person.Properties["name"]
.
Internal IDs
Internal IDs should be treated as opaque values and just sent back to the database as you get them. These ids can be re-used, a best practice is to always look up a node by its business key and label rather than relying on an internal ID.Relationships
Relationship
objects are also IEntity
instances, and therefore also include an Id
, and Properties
.
IRelationship actedIn = row["actedIn"].As<IRelationship>();
long relId = actedIn.Id; // (1)
string type = actedIn.Type; // (2)
IReadOnlyDictionary<string, object> relProperties = actedIn.Properties;
long startId = actedIn.StartNodeId;
long endId = actedIn.EndNodeId;
-
Id
- the internal ID for the relationship. -
Type
- a string representing the type of the relationship, eg -ACTED_IN
-
Properties
- A .NET Map containing all the properties for the relationship.
eg.{"role": "Woody" }
-
StartNodeId
- along
representing the internal ID for the node at the start of the relationship -
EndNodeId
- along
representing the internal ID for the node at the end of the relationship
Paths
If you return a path of nodes and relationships, they will be returned as an instance of the IPath
implementation.
IPath path = row["path"].As<IPath>();
INode start = path.Start; // (1)
INode end = path.End; // (2)
IEnumerable<INode> nodes = path.Nodes; // (3)
IEnumerable<IRelationship> rels = path.Relationships; // (4)
-
Start
- the node starting the path -
End
- the node ending the path -
Nodes
- AnIEnumerable
containing the nodes that form the path -
Relationships
- AnIEnumerable
containingIRelationship
implementations for all segments within the path
Path Segments
A path is split into segments representing each relationship in the path.
For example, say we have a path of (p:Person)-[:ACTED_IN]->(m:Movie)-[:IN_GENRE]->(g:Genre)
, there would be two segments.
-
(p:Person)-[:ACTED_IN]->(m:Movie)
-
(m:Movie)-[:IN_GENRE]->(g:Genre)
The IPath
implementation can be iterated over to access each relationship within the path.
foreach (var segment in path) {
Console.WriteLine(segment.Id);
Console.WriteLine(segment.Type);
Console.WriteLine(segment.StartNodeId);
Console.WriteLine(segment.EndNodeId);
Console.WriteLine(segment.Properties);
}
Check Your Understanding
1. Accessing Node Properties
Which property would you access to log the "name" property for each person to the console?
Select the correct option in the code block below.
IEnumerable<INode> nodes = path.Nodes;
foreach (var node in nodes) {
Console.WriteLine(node./*select:Properties["name"]*/)
}
-
❏ Values["name"]
-
✓ Properties["name"]
-
❏ Get("name")
-
❏ Properties[0]
Hint
The properties
property on a Node is a dictionary.
Solution
The answer is Properties["name"]
.
2. Temporal Accessors
Which of the following functions does the Neo4j Value not support for temporal types.
-
❏
As<Date>
-
❏
As<DateTime>
-
✓
As<IsoDuration>
-
❏
As<LocalDateTime>
Hint
Neo4j supports Date
, DateTime
, LocalDateTime
, Ti`me and LocalTime
temporal types.
Solution
The only unsupported function above is As<IsoDuration>
.
Lesson Summary
In this lesson you have learned how to handle some of the more complex objects returned by a Cypher statement. As we progress through this module, you will use the knowledge gained so far to read data from, and write data back to the database.
In the next Challenge, you will modify code to read from the database.