Search code examples
c#.netcypherneo4jclient

Deserialize into nested objects from Cypher Result in c#


I've been wanting to deserialize automatically from a Cypher Result into their corresponding objects via neo4jclient.

These are the classes I want it to deserialize into

public class QuestionHub
{
    public string Id { get; set; }
    public string Title { get; set; }
    public ICollection<Question> Questions {get;set;}
}

public class Question
{
    public string Id { get; set; }
    public string Title { get; set; }
    public ICollection<Answer> Answers { get;set; }
}

public class Answer
{
    public string Id { get; set; }
    public string Value { get; set; }
}

I know for a fact that this code will put the corresponding QuestionHubs into a list of QuestionHubs. This is exactly what I want, but the problem is that those property that navigate to other classes, are not included.

var hubQuery = graphClient.Cypher
        .Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
        .ReturnDistinct<QuestionHub>("qh");

This is the result As you can see, the questions are not included.

Whenever I do this

var hubQuery = graphClient.Cypher
        .Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
        .ReturnDistinct<QuestionHub>("*");

I get the error

System.ArgumentException: 'Neo4j returned a valid response, however Neo4jClient was unable to deserialize into the object structure you supplied.
    
    First, try and review the exception below to work out what broke.
    
    If it's not obvious, you can ask for help at http://stackoverflow.com/questions/tagged/neo4jclient
    
    Include the full text of this exception, including this message, the stack trace, and all of the inner exception details.
    
    Include the full type definition of Neo4JTests.QuestionHub.
    
    Include this raw JSON, with any sensitive values replaced with non-sensitive equivalents:
    
    { "columns":["a","q","qh"], "data":[[ {"data":{ "Value":"answer9aaf5134-9e73-4681-ba2f-e8224242ff19","Id":"9aaf5134-9e73-4681-ba2f-e8224242ff19" }},{"data":{ "Title":"questiond287a365-364a-4de0-b9f2-574893c1eaaa","Id":"d287a365-364a-4de0-b9f2-574893c1eaaa" }},{"data":{ "Title":"questionHub222a2fbe-6644-491a-b0a1-66df59f05f11","Id":"222a2fbe-6644-491a-b0a1-66df59f05f11" }} ]] } Arg_ParamName_Nam'
Inner Exception
InvalidOperationException: The deserializer is running in single column mode, but the response included multiple columns which indicates a projection instead. If using the fluent Cypher interface, use the overload of Return that takes a lambda or object instead of single string. (The overload with a single string is for an identity, not raw query text: we can't map the columns back out if you just supply raw query text.)

This error is probably because the cypher result gives multiple columns instead of one.

This is what I want to get

If I do this

var hubQuery = graphClient.Cypher
                .Match("(u:User)-[:CREATOR_HUB]-(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
                .With("u, qh, q, COLLECT({Id: a.Id, Value: a.Value}) as answers")
                .With("u, qh, COLLECT({Id: q.Id, Title: q.Title, Answers:answers}) as questions")
                .With("{Creator: {Id:u.Id, FirstName: u.FirstName, LastName: u.LastName}," +
                      "Id: qh.Id, Title: qh.Title, Questions: questions} as result")
                .ReturnDistinct<string>("result");

var hubQueryRes = await hubQuery .ResultsAsync;

            List<QuestionHub> hubList = new List<QuestionHub>();
            foreach (var hub in hubQueryRes )
            {
                hubList .Add(JsonConvert.DeserializeObject<QuestionHub>(hub));
            }

I get what I want, but I needed to write all those .With I want a way to automatically do that without all the .With writing.

I'm looking for a way to automatically deserialize the Cypher result into their corresponding objects with nested objects included.

Is there a way to do this?

Thanks in advance!


Solution

  • You're 100% correct that the reason it didn't create your QuestionHub instance is because the return was entirely in the wrong format for the client to cope with.

    Unfortunately - your with workaround is about the only way - As you're using it to return the output of Cypher into a format that the Json Deserializer can handle.

    The best I can see to do would be this:

    var query = gc.Cypher
        .Match("(qh:QuestionHub)-[:PART_OF]-(q:Question)-[:ANSWER]-(a:Answer)")
        .With("qh, q{.*, Answers: COLLECT(a)} AS qAndA")
        .With("qh{.*, Questions: COLLECT(qAndA)} AS result")
        .Return(result => result.As<QuestionHub>());
    

    Bear in mind, you would also need your ICollection to be either List or IEnumerable to deserialize properly - deserializing into ICollection isn't supported.