1

I am trying to create a runtime lambda function of an arbitrary type, that collects the arguments, that were supplied to it, in a list of objects and passes them to another method of type void Method(List<object> list) to process them. I wrote this code, but got really confused with the result:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace LambdaTest
{
    class LambdaCreator
    {
        ParameterExpression[] Parameters;
        int Index = 0;
        public ParameterExpression Next() 
        {
            return Parameters[Index++];
        }
        public void ResetIndex() 
        {
            Index = 0;
        }

        public void Bar(List<object> parameters)
        {
            foreach (var p in parameters)
            {
                PrintType(p);
            }
        }

        public void PrintType(object arg) 
        {
            Console.WriteLine(arg.GetType().Name);
        }

        public T CreateLambda<T>() where T : class
        {
            var barMethod = GetType().GetMethod("Bar");

            Parameters = typeof(T).GetMethod("Invoke")
                .GetParameters()
                .Select(x => Expression.Parameter(x.ParameterType))
                .ToArray();

            var parametersCount = Expression.Constant(Parameters.Length);

            var listType = typeof(List<object>);
            var list = Expression.Variable(listType);
            var index = Expression.Variable(typeof(int));

            var thisObject = Expression.Constant(this);

            var resetIndex = GetType().GetMethod("ResetIndex");
            var next = GetType().GetMethod("Next");
            var printType = GetType().GetMethod("PrintType");

            var add = listType.GetMethod("Add");

            var breakLabel = Expression.Label();

            var block = Expression.Block(
                new ParameterExpression[] { list, index },
                Expression.Call(thisObject, printType, Parameters.FirstOrDefault()),
                Expression.Call(thisObject, resetIndex),
                Expression.Assign(list, Expression.New(listType)),
                Expression.Loop(
                    Expression.Block(
                        Expression.IfThen(Expression.GreaterThanOrEqual(index, parametersCount), Expression.Break(breakLabel)),
                        Expression.Call(list, add, Expression.Call(thisObject, next)),
                        Expression.AddAssign(index, Expression.Constant(1))
                    ),
                    breakLabel
                ),
                Expression.Call(thisObject, barMethod, list)
            );
            
            var lambda = Expression.Lambda(typeof(T), block, Parameters);
            var compiledLambda = lambda.Compile() as T;
            return compiledLambda;
        }
    }
    class Program
    {
        delegate void Foo(string a, int b);

        static void Main(string[] args)
        {
            var test = new LambdaCreator();
            var l = test.CreateLambda<Foo>();
            l("one", 2);
        }
    }
}

The output of the program was:

String
PrimitiveParameterExpression`1
PrimitiveParameterExpression`1

I was expecting to get:

String
String
Int32

Somehow I lose the values of the arguments when I put them in a list and pass it to the Bar method. Can someone tell me where is the problem I how can I fix it. Or is there another way to collect the arguments and pass them through? I'm really new to this expression trees stuff. Thanks in advance!

0

2 Answers 2

1

You can create a NewArrayExpression using the Parameters array while constructing the lambda function, prior to the Expression.Loop block, and modify the calling code to access the array, like so:

// Declare a paramArray parameter to use inside the Expression.Block
var paramArray = Expression.Parameter(typeof(object[]), "paramArray");

var block = Expression.Block(
    new ParameterExpression[] { list, index, paramArray },  // pass in paramArray here
    Expression.Call(thisObject, printType, Parameters.FirstOrDefault()),
    Expression.Call(thisObject, resetIndex),
    Expression.Assign(list, Expression.New(listType)),

    /* Assign the array - make sure to box value types using Expression.Convert */
    Expression.Assign(
        paramArray,
        Expression.NewArrayInit(
            typeof(object),
            Parameters.Select(p => Expression.Convert(p, typeof(object))))),

    Expression.Loop(
        Expression.Block(
            Expression.IfThen(Expression.GreaterThanOrEqual(index, parametersCount), Expression.Break(breakLabel)),
            //Expression.Call(list, add, Expression.Call(thisObject, next)),
            Expression.Call(list, add, Expression.ArrayIndex(paramArray, index)),  // use the paramArray here
            Expression.AddAssign(index, Expression.Constant(1))
        ),
        breakLabel
    ),
    Expression.Call(thisObject, barMethod, list)
);

The rest is unchanged - this code replaces the var block = ... statement entirely. Works as you specified.

Sign up to request clarification or add additional context in comments.

1 Comment

Great, this is exactly what I was looking for! Thank you!
0

This happens because this call:

Expression.Call(thisObject, printType, Parameters.FirstOrDefault())

Is actually compiled to something like:

this.PrintType(a) 

Where a is your delegate parameter, while this:

Expression.Call(list, add, Expression.Call(thisObject, next))

Is compiled to something like:

this.PrintType(this.Next()) 

One of the options would be to modify print method:

public void PrintType(object arg)
{
    if(arg is ParameterExpression expr)
    {
        Console.WriteLine(expr.Type.Name);
    }
    else
    {
        Console.WriteLine(arg.GetType().Name);
    }
}

To fill list you can just create an corresponding expression:

    var list = Expression.Variable(listType);
    var exprs = new List<Expression>
    {
        Expression.Call(thisObject, resetIndex),
        Expression.Assign(list, Expression.New(listType)),
    };
    
    for (int i = 0; i < @params.Length; i++)
    {
        var ex = Expression.Call(list, add, Expression.Convert(@params[i], typeof(object)));
        exprs.Add(ex);
    }
    
    exprs.Add(Expression.Call(thisObject, barMethod, list));

    var block = Expression.Block(new[] {list}, exprs);

Or use var property = Expression.PropertyOrField(thisObject, nameof(Parameters)); (with changing Parameters to List<object>, assigning new list to it and deleting block parameters) instead of list.

7 Comments

Or just literally call this.Next/this.Reset without wrapping them additionally in another MethodCallExpression. Either way OP is going to have issues due to tracking state in the generator class if they try and create multiple delegates.
Hmm, I wrote the PrintType method, just for example. I intend to process the list in various ways, where I will actually need the values passed to the lambda...
@pinkfloydx33 this will not work cause Next will be called just once, and not in the loop.
@brick you need to split your class in two - one to create lambda and one to handle state. Otherwise you will have problems with multiple calls for your created lambda - for example what should happen in this case: l("one", 2); l("two", 3)? or if the same creator is used to create multiple lambdas.
@GuruStron Yeah, multiple lambdas from a single creator is another issue. I'll think about it. But my main concern at the moment is how to put the values passed to the lambda into the list? Do you have an idea? Is it possible?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.