Return overload fails

2019-02-23 10:26发布

问题:

I'm following this little write up: https://github.com/Readify/Neo4jClient/wiki/cypher but I'm doing it from Powershell. so what I have so far is

[System.Reflection.Assembly]::LoadFrom("C:\...\Newtonsoft.Json.6.0.3\lib\net40\NewtonSoft.Json.dll")
[System.Reflection.Assembly]::LoadFrom("C:\...\Neo4jClient.1.0.0.662\lib\net40\Neo4jClient.dll")

$neo = new-object Neo4jClient.GraphClient(new-object Uri("http://localhost:7474/db/data"))
$q=$neo.Cypher.Match("n").Return({param($m) $m});

with which I would mean to retrieve all nodes in the database. the Return() method is shown in the example to require a lambda expression as a parameter, which in Powershell would be a code-block, however, I get the following error:

Cannot find an overload for "Return" and the argument count: "1". At line:1 char:1 + $q=$neo.Cypher.Match("n").Return({param($m) $m}); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodException + FullyQualifiedErrorId : MethodCountCouldNotFindBest

where am I going wrong?

* Update I *

with the explanation provided by @PetSerAl below, I've managed to get a little further, but I'm still stuck. below I will quote the write up (c#) and then show the equivalent powershell. first we declare a class

public class User
{
    public long Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

my class differs a little

Add-Type -TypeDefinition "public class Project { public string Code; public string Name; public string Parent; public string Lifespan; }"

their Cypher

MATCH (user:User)
RETURN user

their c#

graphClient.Cypher
    .Match("(user:User)")
    .Return(user => user.As<User>())
    .Results

now my cypher

MATCH (n:Project)
RETURN n

...and finally, my attempt at the powershell:

$exp = [System.Linq.Expressions.Expression]
$p = $exp::Constant("Project")
$fn = $exp::TypeAs($p, (new-object Project).GetType())
$return = $exp::Lambda([Func[Project]], $fn, $p)
$neo.Cypher.Match("n").Return($return)

but I get an error

Exception calling "Return" with "1" argument(s): "The expression must be constructed as either an object initializer (for example: n => new MyResultType { Foo = n.Bar }), an anonymous type initializer (for example: n => new { Foo = n.Bar }), a method call (for example: n => n.Count()), or a member accessor (for example: n => n.As().Bar). You cannot supply blocks of code (for example: n => { var a = n + 1; return a; }) or use constructors with arguments (for example: n => new Foo(n)). If you're in F#, tuples are also supported. Parameter name: expression" At line:1 char:1 + $neo.Cypher.Match("n").Return($return) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : ArgumentException

which, for once, is actually very clear and understandable. so what I want is a method call e.g. n => n.Count() and clearly did not achieve that.

help?

* Update II *

so, continuing on with the torturous path of neo4j from powershell, I've given @PetSerAl's second approach a stab and got a little further. here's what I managed to write:

$neopath = "C:\[...]\Neo4jClient.dll"
Add-Type -ReferencedAssemblies $neopath -TypeDefinition @"
    using System;
    using System.Linq.Expressions;
    using Neo4jClient.Cypher;
    public class Project {
        public string Code;
        public string Name;
        public string Parent;
        public string Lifespan;
    };
    public static class NeoExp {
        public static readonly Expression<Func<Neo4jClient.Cypher.ICypherResultItem,Project>> GetProject = (n) => n.As<Project>();
}
"@

which now allows me to do:

$neo.Cypher.Match("n:Project").Return([NeoExp]::GetProject)

and that, miraculously, works! except it brings me back no data:

Results ResultsAsync Query                         Client 
------- ------------ -----                         ------                                                                      
                     Neo4jClient.Cypher.CypherQ... Neo4jClient.GraphClient

and I know I have projects in the database... so what could the issue be now?

* Update III *

wow, so close but still not done. as per the latest suggestion from @PetSerAl. I tried:

$neo.Cypher.Match("n:Project").Return([NeoExp]::GetProject).get_Results()

which yielded an illuminating error:

Exception calling "get_Results" with "0" argument(s): "The graph client is not connected to the server. Call the Connect method first."

so that made it clear I first needed to do:

$neo.Connect()

also, I needed parentheses around the query's match clause:

$neo.Cypher.Match("(n:Project)").Return([NeoExp]::GetProject)

now I get 27 results back as expected in the .Results field... however, the results are all blank. so I think maybe it has to do with the n.As<Project>() where perhaps my class isn't defined properly and that fails. any thoughts?

* Update IV *

ok, got it. the Project class needs to have properties, not fields:

    public class Project {
        public string Code { get; set; }
        public string Name { get; set; }
        public string Parent { get; set; }
        public string Lifespan { get; set; }
    };

and that's it. I have data. yea!!!

@PetSelAl: I owe you a beer

回答1:

There are two problems with Return method:

  1. It accepts argument of expression tree type, rather then compiled delegate type. And PowerShell does not have an easy way to create expression tree from a ScriptBlock. So, you have to create expression tree by hands or use string overload.
  2. string overload does not allows PowerShell to infer generic parameter for method, and PowerShell syntax does not allows to specifying generic parameter explicitly. So, PowerShell can not call string overload of Return method directly. You have to use some workaround to call it, for example, call it thru Reflection.

Sample how can you create a simple expression tree ((a,b) => a*2+b) in PowerShell:

# First way: messing with [System.Linq.Expressions.Expression]
$a=[System.Linq.Expressions.Expression]::Parameter([int],'a')
$b=[System.Linq.Expressions.Expression]::Parameter([int],'b')
$2=[System.Linq.Expressions.Expression]::Constant(2)

$Body=[System.Linq.Expressions.Expression]::Add([System.Linq.Expressions.Expression]::Multiply($a,$2),$b)
$Sum=[System.Linq.Expressions.Expression]::Lambda([Func[int,int,int]],$Body,$a,$b)
$Sum

# Second way: using help of C#
Add-Type -TypeDefinition @'
    using System;
    using System.Linq.Expressions;
    public static class MyExpression {
        public static readonly Expression<Func<int,int,int>> Sum=(a,b) => a*2+b;
    }
'@
[MyExpression]::Sum


回答2:

I know this is not exactly what you're asking, and might not even be what you want, but you could create your own C# class right inside PowerShell using Add-Type. It might be easier to implement it that way and provide simple methods you can use within the PowerShell code if what you're writing relies on a lot of C#-specific things.

This example is taken straight from the link above:

$source = @"
public class BasicTest
{
  public static int Add(int a, int b)
  {
    return (a + b);
  }
  public int Multiply(int a, int b)
  {
    return (a * b);
  }
}
"@
Add-Type -TypeDefinition $source
[BasicTest]::Add(4, 3)
$basicTestObject = New-Object BasicTest
$basicTestObject.Multiply(5, 2)


回答3:

The best I can tell is that you're using it wrong. Your object structure should be your Neo4jClient object, which should then have a few properties and methods. I am pretty sure that Return is a property, not a method. So I'm thinking something more like:

$neo = new-object Neo4jClient.GraphClient(new-object Uri("http://localhost:7474/db/data"))
$neo.Cypher.Match = "n"
$neo.Cypher.Return = {param($m) $m}
$q = $neo.Cypher.Results()

There you create your object, you define the Match filter, define what you want it to return (everything from the looks of it), and then store the results in the $q variable. I'm pretty sure that should be the same as:

SELECT * FROM Uri("http://localhost:7474/db/data")) WHERE "n"

I also kind of wonder about your Match criteria, since they seem to be specifying property/value pairs in their examples and you just give one of the two. If that fails I would strongly suggest doing a $neo.Cypher | Get-Member to see what properties you have, what they're typed as, and what methods you have.

Edit: Ok, so I did look at the link. I also downloaded the libraries, loaded them into PowerShell, and looked at the .Net objects. Return is indeed a method, and the overloads for it are ridiculous. There are 37 overloads for it, the longest of which is almost 750 characters. Most of them are ICypherResultItem expressions, but the simplest is (string identity). Might I suggest simply trying Return("*")?



回答4:

a million thanks to @PetSerAl who explained the problem well in its essence, and who held my hand through its resolution.

to restate, the problem is that Powershell has no built-in mechanism to call generic methods and the Neo4jClient library's .Return method overloads are all generic. Specifically, Powershell provides no means of supplying the type of the method.

so there are several ways to solve this:

  • one solution is to use reflection to make the call, but the task is a bit tricky. thankfully the approach has been tackled by a project available on tech net. see: https://gallery.technet.microsoft.com/scriptcenter/Invoke-Generic-Methods-bf7675af

  • the second solution suggested in @PetSerAl's reply needed a little help but with @ChrisSkardon's help I got it to work. see here: Constructing a method call

  • and the third solution (see the Updates in the original post) which relies on creating a c# class with a method where the generic method can be called

I expect to be documenting this solution in the wiki for Neo4jClient and I hope the effort documented here is helpful to others. Thanks again @PetSerAl for all your help