C# Custom Node (dll) - How to return multiple Outputs from 1 Function?

Dear All,

I write a c# custom node (dll) that queries web pages and is supposed to create 2 lists as a result - assigned to output ports 0 + 1.

I understand that you usually create ONE function call per output and then assign the result with BuildAssignment.

However, it makes no sense for me to query the web pages TWO times - this would take way too long and duplicate a lot of the code and querying for no reason!

I want to do this ONE time only and afterwards take the result and assign to both ports.

e.g.

var functionCall = AstFactory.BuildFunctionCall(
new Func<string, List< List < object > >(Function_GetWebpages),
new List() { inputAstNodes[0] });
return new[] {
AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall[0]),
AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall[1])
};

The above obviously doesn’t work since the function returns an AssociativeNode and not a List with 2 nested Lists.

How can this work with just ONE function call? How can I assign the nested lists to the 2 outputs?

Thanks a lot, F

1 Like

hmm, not sure perhaps you want to use AstFactory. BuildIndexExpression

1 Like

I don’t have a AstFactory.BuildIndexExpression in 1.3.2 ProtoCore??

Why can’t this be a zero touch node instead you are using a NodeModel. Please look at GitHub queries that I did here: https://github.com/ksobon/archilab/blob/master/archilab/GitHub/GitHub.cs Pay attention to MultiReturn class attributes and how they are used with Dictionaries. That should work just fine.

2 Likes

Thanks for the good examples Konrad.
This would usually work… however, my node requires a custom UI as well, hence I don’t think I can get around the AstFactory.

@fmiller @Konrad_K_Sobon you can do it this way:

filter node does something similar I think to generate included and excluded items.

2 Likes

Thanks a lot Michael!

I just finished trying out the AstFactory.BuildIndexExpression function and it works with 2.0, e.g.

var functionCall = AstFactory.BuildFunctionCall(
new Func<string, List< List < object > >(Function_GetWebpages),
new List() { inputAstNodes[0] });
var functionCall0 = AstFactory.BuildIndexExpression(functionCall, AstFactory.BuildIntNode(0));
var functionCall1 = AstFactory.BuildIndexExpression(functionCall, AstFactory.BuildIntNode(1));
return new[] {
AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), functionCall0),
AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(1), functionCall1)
};

I will have a look at your 2nd example as well and see if this does the trick for 1.3.
[or is 2.0 release date rather close and 1.3 can be discarded?]

1 Like

I tested the 2nd proposal from Michael as well and it works with 1.3.0 so no need to switch to 2.0. yet.
Thanks a lot again Michael!!!

1 Like

What if the function needs to return 2 different types of lists, e.g. a list of objects and a list of dictionaries?

Returning them from the function would work with using a Tuple, e.g.
public static Tuple<List<object>,List<Dictionary<string, object>>> GetObjects(string s) { }

However, how can those 2 lists be “extracted” from the Tuple in BuildOutputAst and assigned to port 0 + 1?
The solution mentioned for 1.3 (see below) doesn’t work for this as it’s no List / Array.

public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes) 
{
   string packedId = "__temp" + AstIdentifierGuid;
   return new[] {

      AstFactory.BuildAssignment(AstFactory.BuildIdentifier(packedId), AstFactory.BuildFunctionCall(
         new Func<string, Tuple<List<object>,List<Dictionary<string, object>>>>(GetObjects),
         new List<AssociativeNode>() { inputAstNodes[0] } )),

      AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), new IdentifierNode(packedId) {
         ArrayDimensions = new ArrayNode { Expr = AstFactory.BuildIntNode(0) } }),

      AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(1), new IdentifierNode(packedId) {
         ArrayDimensions = new ArrayNode { Expr = AstFactory.BuildIntNode(1) } })

   };                      
}

Best regards, Eckart

return a list<list<object>>- I don’t think designscript has tuple as a type or a conversion from .net tuple to a designscript type. Another option is to return a dictionary … but getting keys in a dictionary is kind of tricky, and has changed a lot between 1.3 and 2.0. I would use the nested list of objects.

That’s what I tried first and it works with simple types of different format, e.g. list 0 = string, list 1 = int but as soon as I use a dictionary as list 1 it breaks it and output port 1 remains null.

The warning message displayed on the node is:
Warning: Converting an array to var would cause array rank reduction and is not permitted

So I am not sure this nested list approach is working for dictionaries?
Or is something else wrong / needs to be fixed?

FUNCTION:

public static List<List<object>> GetObjects(string s)
{
   List<List<object>> r = new List<List<object>>();
   List<object> r0 = new List<object>();
   List<object> r1 = new List<object>();

   r0.Add("string1");
   r0.Add("string2");
   r0.Add("string3");

   r1.Add(new Dictionary<string, string>() { { "1", "A" }, { "2", "B" }, { "3", "C" } });  // this breaks it

   r.Add(r0);
   r.Add(r1);
   return r;
}

AST:

public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
{
   string packedId = "__temp" + AstIdentifierGuid;
   return new[] {

      AstFactory.BuildAssignment(AstFactory.BuildIdentifier(packedId), AstFactory.BuildFunctionCall(
         new Func<string, List<List<object>>>(GetObjects),
         new List<AssociativeNode>() { inputAstNodes[0] } )),

      AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(0), new IdentifierNode(packedId) {
         ArrayDimensions = new ArrayNode { Expr = AstFactory.BuildIntNode(0) } }),

      AstFactory.BuildAssignment(GetAstIdentifierForOutputIndex(1), new IdentifierNode(packedId) {
         ArrayDimensions = new ArrayNode { Expr = AstFactory.BuildIntNode(1) } })

   };          
}

this actually works in dynamo 2.0 where .net dictionary marshalling to the designScript dictionary type is supported. Still need to mess with it in 1.3.

one thing you could do is wrap your dictionary in your own type, like myDictionary() to test if it’s some marshaling problem when dynamo encounters a .net dictionary. (my guess)

Thanks Michael, I will try that out… pity since those nodes already exist in 2.0 - It’s time for Dynamo 2.0 release I guess :wink:

what about using the interface list return IList<IList> wouldnt this return also work? …or as you write a dictionary -> as an IDictionary which is what I use…

In my coding I try to return everything via Interfaces, just like I can see it is done in many cases in the dynamo nodes!?

eg.

[MultiReturn(new[] { "Return1", "Return2" })]
public static IDictionary SomeMethod(IList argument)
{
do something...

    return new Dictionary<string, object>
    {
        {"Return1", value1},
        {"Return2", value2}
    };
}

could also be an IEnumerable like in the below case…

public static IEnumerable Flatten(IEnumerable lst)
{
    foreach (object item in lst)
    {
        IEnumerable sub = item as IEnumerable;
        if (sub != null)
        {
            foreach (object i in sub)
            {
                yield return i;
            }
        }
        else
        {
            yield return item;
        }
    }
}

Thanks a lot for this example. I will try it out as well to find the most suitable way.