1
votes

I've been playing with neo4JClient for a few days now and have a working playground of a small selection of the data entities I want to model. What you see here is an isolated example to try and figure out what is wrong.

the code supplied should copy and paste and just run with the right references available;

The code illustrates the the core methods work correctly outside of a transaction but fail on the first attempt to create a new relationship between two new nodes inside the same transaction.

I don't know if I'm falling into newbie trap for a) neo4j in general, neo4jClient specifically , or there is a genuine problem with transaction handling. I am assuming that I am wrong, and that my thinking is lacking but I cannot find another related problem anywhere to give me a clue.

Put simply my use case is this;

As a new user I want to register as an owner with my current identity and be able to add my list of related assets to my portfolio.

I know that this may not be either the correct or the most efficient way of doing this suggestions would very much be appreciated.

The following code should illustrate the use cases that work and the on that fails;

The exception I get is;

System.InvalidOperationException was unhandled HResult=-2146233079
Message=Cannot be done inside a transaction scope.
Source=Neo4jClient StackTrace: at Neo4jClient.GraphClient.CheckTransactionEnvironmentWithPolicy(IExecutionPolicy policy) in D:\temp\384a765\Neo4jClient\GraphClient.cs:line 797 at Neo4jClient.GraphClient.CreateRelationship[TSourceNode,TRelationship](NodeReference`1 sourceNodeReference, TRelationship relationship) in D:\temp\384a765\Neo4jClient\GraphClient.cs:line 350 at ConsoleApplication1.Example.CreateOwnerNode(IGraphClient client, Owner owner, Identity identity) in . . . InnerException:

using System;
using System.Linq;
using System.Transactions;
using Neo4jClient;

namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var example = new Example();
        }
    }

    public class Example
    {
        public Example()
        {
            var rootUri = new Uri("http://localhost:7474/db/data/");
            var username = "neo4j";
            var neo4jneo4j = "neo4j";
            IGraphClient client = new GraphClient(rootUri, username, neo4jneo4j);
            client.Connect();

            Node<Owner> ownerNode;
            Node<Identity> identityNode;

            // whole thing outside tranaction
            ownerNode = CreateOwnerNode(client, new Owner(), new Identity());


            // individually outside transaction
            ownerNode = CreateOwner(client, new Owner());
            identityNode = CreateIdentity(client, new Identity());
            GiveOwnerAnIdentity(client, ownerNode, identityNode);


            // individually inside a transaction
            using (var scope = new TransactionScope())
            {
                ownerNode = CreateOwner(client, new Owner());
                identityNode = CreateIdentity(client, new Identity());
                GiveOwnerAnIdentity(client, ownerNode, identityNode);
                scope.Complete();
            }

            // whole thing inside a transaction
            using (var scope = new TransactionScope())
            {
                ownerNode = CreateOwnerNode(client, new Owner(), new Identity());
                scope.Complete();
            }

            //TODO: Something else with ownerNode
        }

        public void GiveOwnerAnIdentity(IGraphClient client, Node<Owner> ownerNode, Node<Identity> identityNode)
        {
            client.CreateRelationship(ownerNode.Reference, new Has(identityNode.Reference));
        }

        public Node<Identity> CreateIdentity(IGraphClient client, Identity identity)
        {
            var identityKey = KeyFor<Identity>();

            return client.Cypher.Create(identityKey)
                .WithParams(new { identity })
                .Return(o => o.Node<Identity>())
                .Results
                .Single();
        }

        public Node<Owner> CreateOwner(IGraphClient client, Owner owner)
        {  var ownerKey = KeyFor<Owner>();
           return client.Cypher.Create(ownerKey)
                   .WithParams(new { owner })
                   .Return(o => o.Node<Owner>())
                   .Results.Single();

        }


        /// <summary>
        ///     Create a node for an owner along with its nominated identity, relate the owner as having an identity
        /// </summary>
        /// <param name="client">The <see cref="Neo4jClient" /> instance</param>
        /// <param name="owner">The <see cref="Identity" /> instance</param>
        /// <param name="identity">The <see cref="Identity" /> instance</param>
        /// <returns>The created <see cref="Owner" /> node instance for additional relationships</returns>
        public Node<Owner> CreateOwnerNode(IGraphClient client, Owner owner, Identity identity)
        {
            var ownerKey = KeyFor<Owner>();
            var identityKey = KeyFor<Identity>();

            var ownerNode =
                client.Cypher.Create(ownerKey)
                    .WithParams(new {owner})
                    .Return(o => o.Node<Owner>())
                    .Results.Single();

            var identityNode = client.Cypher.Create(identityKey)
                .WithParams(new {identity})
                .Return(o => o.Node<Identity>())
                .Results
                .Single();

            client.CreateRelationship(ownerNode.Reference, new Has(identityNode.Reference));
            return ownerNode;
        }

        /// <summary>
        ///     Conform a Cypher create text for a type
        /// </summary>
        /// <typeparam name="TObject">The type to handle</typeparam>
        /// <returns>A string like "{o:TObject {tobject})</returns>
        public string KeyFor<TObject>()
        {
            var name = typeof(TObject).Name;
            return $"(o:{name} {{{name.ToLower()}}})";
        }

        public abstract class Nodebase
        {
            public Guid Id { get; set; }

            public Nodebase()
            {
                Id = Guid.NewGuid(); // make sure each node is always uniquely identifiable
            }
        }
        /// <summary>
        ///     Owner node , properties to be added later
        /// </summary>
        public class Owner
        {
        }

        /// <summary>
        ///     Identity  node , properties to be added later
        /// </summary>
        public class Identity
        {
        }

        /// <summary>
        ///     The <see cref="Owner" /> Has an <see cref="Identity" />
        /// </summary>
        public class Has : Relationship,
            IRelationshipAllowingSourceNode<Owner>,
            IRelationshipAllowingTargetNode<Identity>
        {
            internal Has(NodeReference<Identity> targetNode)
                : base(targetNode)
            {
            }

            public override string RelationshipTypeKey => GetType().Name.ToUpper();
        }
    }
}

UPDATE:

OK, an some more information but not a solution, yet.

As ever, please weigh in if I am barking up the wrong tree.

More of a clue as to why transactions are so hard to work with (so far).

I corrected my code according to Chris Skardons reply, which is correct and it solves the creation problem but does not solve the principle problem of creating the required objects within a transaction and getting the node reference back. It does however actually create the nodes and relationship. I think that there is a bug in neo4jClient somewhere.

The request actually succeeds but the handling at the client end falls over, probably because I'm asking for a node reference to use later in my code.

This hopefully final issue is now centred on the following method within the GraphClient processing for transactions.

It appears that when you wrap a transaction around a Cypher query it all steps outside the standard Cypher API method handling and puts everything through the Transaction API.

This results in a very different response and that I believe is where its going wrong.

I downloaded the entire Neo4jClient code base and hooked it directly into my example code solution instead of the NuGet package so I could step through all the code to the HttpClient if necessary.

I also hooked in fiddler to watch the REST messages. More on that below.

These two fiddler sessions show what is going on in more detail; (Please excuse the long posting as its relevant data to understand what is happening.)

Outside of a transaction

POST http : //localhost:7474/db/data/cypher HTTP/1.1
Accept : application / json;
stream = true
    X - Stream : true
    User - Agent : Neo4jClient / 0.0.0.0
    Authorization : Basic bmVvNGo6bmVvNGpuZW80ag ==
    Content - Type : application / json;
charset = utf - 8
    Host : localhost : 7474
    Content - Length : 174
    Expect : 100 - continue
    {

    "query" : "CREATE (o:Owner {owner})\r\nCREATE (i:Identity {identity})\r\nCREATE (o)-[:HAS]->(i)\r\nRETURN o",


    "params" : {
        "owner" : {},
        "identity" : {}
    }
}
HTTP / 1.1 200 OK
Date : Wed, 18 May 2016 12 : 03 : 57 GMT

Content - Type : application / json;
charset = UTF - 8;
stream = true
    Access - Control - Allow - Origin :  *
    Content - Length : 1180
    Server : Jetty(9.2.9.v20150224)
    {
    "columns" : ["o"],
    "data" : [[{
                "extensions" : {},
                "metadata" : {
                    "id" : 53044,
                    "labels" : ["Owner"]
                },
                "paged_traverse" : "http://localhost:7474/db/data/node/53044/paged/traverse/{returnType}{?pageSize,leaseTime}",


                "outgoing_relationships" : "http://localhost:7474/db/data/node/53044/relationships/out",
                "outgoing_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/out/{-list|&|types}",
                "create_relationship" : "http://localhost:7474/db/data/node/53044/relationships",

                "labels" : "http://localhost:7474/db/data/node/53044/labels",
                "traverse" : "http://localhost:7474/db/data/node/53044/traverse/{returnType}",
                "all_relationships" : "http://localhost:7474/db/data/node/53044/relationships/all",
                "all_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/all/{-list|&|types}",
                "property" : "http://localhost:7474/db/data/node/53044/properties/{key}",
                "self" : "http://localhost:7474/db/data/node/53044",
                "incoming_relationships" : "http://localhost:7474/db/data/node/53044/relationships/in",
                "properties" : "http://localhost:7474/db/data/node/53044/properties",
                "incoming_typed_relationships" : "http://localhost:7474/db/data/node/53044/relationships/in/{-list|&|types}",
                "data" : {}
            }
        ]]
}

Inside a transaction

POST http : //localhost:7474/db/data/transaction HTTP/1.1
Accept : application / json;
stream = true
    X - Stream : true
    User - Agent : Neo4jClient / 0.0.0.0
    Authorization : Basic bmVvNGo6bmVvNGpuZW80ag ==
    Content - Type : application / json;
charset = utf - 8
    Host : localhost : 7474
    Content - Length : 273
    Expect : 100 - continue
    {
    "statements" : [{
            "statement" : "CREATE (o:Owner {owner})\r\nCREATE (i:Identity {identity})\r\nCREATE (o)-[:HAS]->(i)\r\nRETURN o",
            "resultDataContents" : [],
            "parameters" : {

                "owner" : {},
                "identity" : {}
            }
        }
    ]

}
HTTP / 1.1 201 Created
Date : Wed, 18 May 2016 12 : 04 : 00 GMT
Location : http : //localhost:7474/db/data/transaction/585
Content - Type : application / json
Access - Control - Allow - Origin :  *
Content - Length : 241
Server : Jetty(9.2.9.v20150224)
{
    "commit" : "http://localhost:7474/db/data/transaction/585/commit",
    "results" : [{
            "columns" : ["o"],
            "data" : [{
                    "row" : [{}
                    ],
                    "meta" : [{
                            "id" : 53046,
                            "type" : "node",
                            "deleted" : false
                        }
                    ]
                }
            ]
        }
    ],
    "transaction" : {
        "expires" : "Wed, 18 May 2016 12:05:00 +0000"
    },
    "errors" : []
}

I tracked a problem down to this method;

public class CypherJsonDeserializer<TResult>

IEnumerable<TResult> ParseInSingleColumnMode(DeserializationContext context, JToken root, string[] columnNames, TypeMapping[] jsonTypeMappings)

Near the end of that method the line;

 var parsed = CommonDeserializerMethods.CreateAndMap(context, newType, elementToParse, jsonTypeMappings, 0);

returns a corrctly formed 'parsed' variable when NOT in a transaction (ie. it calls the Cypher API URL), but does not populate the properties of that variable when inside a transaction and is using the transaction API.

My question is, considering that the transaction returns very different data should it be calling this method at all?

After that point it all blows up in your face. I don't know enough about the code intention at this point to say more. I have less than a week under my belt as a neo4jClient user.

Using neo4jClient with fiddler and localhost

In my investigations I also found other issues related to hooking up Fiddler to see what is going on.

I used the Fiddler Rules trick to name my local URL to 'localneoj4' instead of localhost:7474, and use that new name in the client.Connect method, so that I could see the local traffic.

var rootUri = "http://localneo4j/db/data";
    IGraphClient client = new GraphClient(rootUri, username, password);
    client.Connect();

As suggested here and added this to my Rules;

 if (oSession.HostnameIs("localneo4j")) {
     oSession.host = "localhost:7474"; }

this caused a bug to creep out inside

public class NeoServerConfiguration          
internal static async Task<NeoServerConfiguration> GetConfigurationAsync(Uri rootUri, string username, string password, ExecutionConfiguration executionConfiguration)  

This has probably effected many developers going down this route.

Because fiddlers proxy effect is casing a disconnect between the neo4jClient's idea of the address and the servers idea of the address.

A string length mismatching problem occurs when processing the URI's coming back from the connect response on the following lines because all of the result.* properties start with 'http://localhost:7474/db/data/' but rootUriWithoutUserInfo starts with http://localneo4j/db/data/'

    var baseUriLengthToTrim = rootUriWithoutUserInfo.AbsoluteUri.Length - 1;

    result.Batch = result.Batch.Substring(baseUriLengthToTrim);
    result.Node = result.Node.Substring(baseUriLengthToTrim);
    result.NodeIndex = result.NodeIndex.Substring(baseUriLengthToTrim);
    result.Relationship = "/relationship"; //Doesn't come in on the Service Root
    result.RelationshipIndex = result.RelationshipIndex.Substring(baseUriLengthToTrim);
    result.ExtensionsInfo = result.ExtensionsInfo.Substring(baseUriLengthToTrim);

A quick work around for this issue is that the app name you use in the fiddler rule matches its length with 'localhost:7474'

A better fix might be (and I've tested it for < == and > relative address lengths) as a means of cutting out the protocol server address and port entirely from concern, but that's down to the code owner I guess;

    private static string CombineTrailingSegments(string result, int uriSegementsToSkip)
    {
        return new Uri(result).Segments.Skip(uriSegementsToSkip).Aggregate(@"/", (current, item) =>{ return current += item;});
    }

then

    var uriSegementsToSkip = rootUriWithoutUserInfo.Segments.Length;    // which counts the db/data and adjusts for server configuration
    result.Batch = CombineTrailingSegments(result.Batch, uriSegementsToSkip);
    ...
1
You're correct - the TX endpoint gives back a different response - it trims all the data that you get from the Cypher end point. I think your problem stems from the fact you are trying to coerce a response into a Node<T> type - when you can't as it isn't. You should raise your issues on the GitHub page for Neo4jClient - as otherwise they'll get lost in stackoverflow.Charlotte Skardon
Thanks Chris. Will do. I suspected that might be the case, I wanted to end the story here though for any other newbie coming along behind :-)user2960136

1 Answers

1
votes

You get the exception because you are using the old API calls, the TransactionScope implementation is for Cypher calls, basically anything within: client.Cypher.

Generally, Neo4jClient has moved away from Node<T> (useful in some specific cases) and Relationship types. Your CreateOwnerNode I think should look more like this:

public Node<Owner> CreateOwnerNode(IGraphClient client, Owner owner, Identity identity)
{
    var query = client.Cypher
        .Create($"(o:{GetLabel<Owner>()} {{owner}})")
        .Create($"(i:{GetLabel<Identity>()} {{identity}})")
        .WithParams(new {owner, identity})
        .Create("(o)-[:HAS]->(i)")
        .Return(o => o.As<Node<Owner>>());

    return query.Results.Single();
}

private string GetLabel<TObject>()
{
    return typeof(TObject).Name;
}

You want to try to get as much done in a single query as you can, and in the case you're trying - you would have made 3 calls to the DB, this will do it in one.

I think it's worth maybe doing some of these things using just plain Cypher first to get used to it, there shouldn't be any reason you need to use the CreateRelationship stuff - in fact, I'd say any time you leave the .Cypher bit - double check to make sure it's really what you want to do.