Recreating objects with data retrieved by Neo4j .NET driver

Hi
I am new to both Neo4j and the Neo4j .NET driver. I am serializing and deserializing specific “patterns” from .NET to Neo4j and back (which is why I am trying to keep it simple without any other libraries). An example is this:

public class Car
{
	public string Name { get; set; }
	public IList<Engine> Engines { get; set; }
}

public class Engine
{
	public string Name { get; set; }
	public Maker Maker { get; set; }
	public Material Material { get; set; }
}

public class Maker
{
	public string Name { get; set; }
}

public class Material
{
	public string Name { get; set; }
}

My data in Neo4j looks like this (create statement attached in the end):

Pattern

I am trying to re-create the Car based upon the above data. However, my query:

MATCH(from:Car { Name: "Hybrid" })-[r:USES|CREATOR|MADE_OF*1..2]->(to) RETURN from, r, to

Returns an IList with 5 records that seems overly difficult to use to re-create the above Car object, which suggest I am on the wrong path. My current attempt is not working yet and growing in complexity and works by iterating through the List and looking at the type of relationship, the to node and ascertaining where it is in the pattern.

I can re-create the Car as this is the from Node, however the Engine, Maker and Material is causing me problems. It seems like I need a combination of Node and Relationship to re-create the correct object, however I don't see how.

Is there a better path? Either by using Cypher to return an object that is easier to deserialize (for example containing the hierarchy of the objects) or some way to return subgraphs so the results are easier to work with?

Thank you in advance!

Best regards
Andreas:

Create.txt (478 Bytes)

Hello!

What does your C# look like at the moment?

All the best

Chris

Hi Chris!

Thank you for your reply :slight_smile:

I was afraid you were going to ask, as it is not pretty but here goes (and as I wrote it is not working yet):

I have split up my query in two parts to better control / understand it:

//First let's get the Car and convert it to a dictionary so we can add properties for later deserialization.
var carQuery = "MATCH (car:Car { Name:'Hybrid' }) RETURN car";
var carResult = await session.RunAsync(carQuery);
var carRecord = await carResult.ToListAsync();
var carView = carRecord?.First()["car"].As<INode>().Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

//Lets get all the connected nodes needed to build up the rest of the Car
var relatedObjectsQuery = "MATCH(car:Car { Name: 'Hybrid' })-[r:USES|CREATOR|MADE_OF*1..2]->(to) RETURN r, to";
var relatedObjectsResult = await session.RunAsync(relatedObjectsQuery);
var relatedObjectsRecords = await relatedObjectsResult.ToListAsync();

//Here is where it gets fuzzy and the below is NOT working and feels like a wrong way to go about it.
foreach (var relatedObjectsRecord in relatedObjectsRecords)
{
	var relationships = relatedObjectsRecord["r"].As<List<IRelationship>>();

	if (relationships.Count == 1)
	{
		//If count is 1 and relationship is USES then it must be an Engine
		if (relationships.Single().Type == "USES")
		{
			carView.Add("engine", relatedObjectsRecord["to"].As<INode>().Properties);
		}
	}

	if (relationships.Count == 2)
	{
		//more wrong stuff...
	}
}

I try to build up my object from traversing the results of the query, where the correct way (or at least the one in control) seems to be able to specifically pinpoint "patterns", for example, this is an Engine with Maker and Material and connect that to the car as in the below pseudocode:

var engines = Somehow parse the result to get a list of Engines with Maker and Material.

var car = new Car
{
	Name = Gotton from the Neo4j result,
	Engines = engines,
}

Am I not sure if the best way is to solve this using fewer Cypher statements and parse the results in C# or try to write multiple Cypher queries and then put the results together?

Best regards
Andreas

I got something working, by splitting up the query. As written above I use this query to get the root node Car:

MATCH (car:Car { Name:'Hybrid' }) 
RETURN car

Although this makes my application "chattier" I can get the individual Engines and their data by using the below query:

MATCH (from:Car {Name: 'Hybrid'})-[r1:USES]->(engine)-[r2:CREATOR|MADE_OF]->(data) 
RETURN engine, collect(data)

I can convert the result of the above query to Engine objects and then add them to the List of Engines in the car gotten in the first query. Not pretty or effective, but it works until I become more experienced :)

Best regards
Andreas

Are you expecting to get a single 'car' instance from it?

Yes the "Car" instance is unique. In the real application I am using a unique Guid for each instance of Car.

Hey Andreas,

Sorry for the time delay - I got waylaid - ok, so you can do it in one query, and to make it a bit easier to read, I've used Neo4j.Driver.Extensions - which you can find in Nuget - and allows you to do a bit of ToObject type stuff.

Anyhews, the only difference to your classes was the addition of the [Neo4jProperty] attribute on the Name properties - as you use lowercase property names for some, and Uppercase for others. (As an aside - from a .NET perspective - it's easier to use Uppercase, as it matches the coding style).

All the best

Chris

async Task Main()
{
	var driver = GraphDatabase.Driver("neo4j://localhost:7687", AuthTokens.Basic("neo4j", "neo"));
	var session = driver.AsyncSession();

	var executionResult = await session.RunAsync(@"MATCH (car:Car { Name: 'Hybrid' })-[:USES]->(engine:Engine)-[:CREATOR]->(maker:Maker)
OPTIONAL MATCH(engine) -[:MADE_OF]->(material: Material)
WITH car, { Engine: engine, Maker: maker, Material: material}
	AS bits
WITH car, collect(bits) AS bits
RETURN car, bits");

	var results = await executionResult.ToListAsync();

	var cars = new List<Car>();
	foreach (var result in results)
	{
		var car = result["car"].As<INode>().ToObject<Car>();
		car.Engines = new List<Engine>();
		foreach (IDictionary<string, object> bit in result["bits"].As<List<IDictionary<string, object>>>())
		{
			var engine = bit["Engine"].As<INode>().ToObject<Engine>();
			engine.Maker = bit["Maker"].As<INode>().ToObject<Maker>();
			bit["Material"].TryAs<INode>(out var materialNode);
			engine.Material = materialNode?.ToObject<Material>();
			car.Engines.Add(engine);
		}
		cars.Add(car);
	}
}


public class Car
{
		public string Name { get; set; }
	public IList<Engine> Engines { get; set; } = new List<Engine>();
}

public class Engine
{
	[Neo4jProperty(Name="name")]
	public string Name { get; set; }
	public Maker Maker { get; set; }
	public Material Material { get; set; }
}

public class Maker
{
	[Neo4jProperty(Name="name")]
	public string Name { get; set; }
}

public class Material
{
	[Neo4jProperty(Name="name")]
	public string Name { get; set; }
}

Interesting, Neo4j.Driver.Extensions is oddly similar to my NODES 2019 submission SchematicNeo4j that's been available as a nuget package since mid 2019.

@charlotte.skardon .NET support and mapping from graph to C# is one of the things I really enjoy (enough to spend my own free time on it). You all hiring for your .NET team? :slight_smile:

Hey Chris

Sorry for my late reply.

Thank you very much for your help! By following your example, I managed to reduce the complexity of my code and handle it all in a single query :slight_smile:

Best regards
Andreas