1

I have a list of this simple model:

// model:
public class Test {
    public int ID { get; set; }
    public string Name { get; set; }
}

as var list = new List<Test>() { /* some items here */ };. And I'm generating a DataTable from the list by this snippet:

var dataTable = new DataTable();
dataTable.Columns.Add("ID", typeof(int));
dataTable.Columns.Add("Name", typeof(string));
foreach (var item in list) {
    var dr = dataTable.NewRow();
    dr["ID"] = item.ID;
    dr["Name"] = item.Name;
    dataTable.Rows.Add(dr);
}

Now I'm trying to generating some expression-tree to do the above snippet at run-time (in a generic way). However, my attempt got me here:

    private static Action<DataTable, IEnumerable<T>> GetAction() {
        if (_filler != null)
            return;
        var type = typeof(T);
        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        var tableParam = Expression.Parameter(typeof(DataTable), "targetTable");
        var rowsParam = Expression.Parameter(typeof(IEnumerable<T>), "rows");

        var loopVariable = Expression.Parameter(typeof(T), "item");

        var columnsVariable = Expression.Parameter(typeof(DataColumnCollection), "columns");
        var columnsAssign = Expression.Assign(columnsVariable,
            Expression.Property(tableParam, typeof(DataTable).GetProperty("Columns")));


        var headerExpressions = new List<Expression>();
        var bodyExpressions = new List<Expression>();

        var newRowParam = Expression.Parameter(typeof(DataRow), "currentRow");
        var newRowAssign = Expression.Assign(newRowParam, Expression.Call(tableParam, typeof(DataTable).GetMethod("NewRow")));

        foreach (var prop in props) {
            var getMethod = prop.GetGetMethod(false);
            if (getMethod == null)
                continue;
            var attr = prop.GetCustomAttribute<UdtColumnAttribute>();
            var name = attr == null ? prop.Name : attr.ColumnName;

            var headerNameParam = Expression.Parameter(typeof(string), "columnName");
            var headerNameAssign = Expression.Assign(headerNameParam, Expression.Constant(name, typeof(string)));

            var headerTypeParam = Expression.Parameter(typeof(Type), "columnType");
            var headerTypeAssign = Expression.Assign(headerTypeParam, Expression.Constant(prop.PropertyType, typeof(Type)));

            var columnsAddMethod = Expression.Call(columnsVariable,
                typeof(DataColumnCollection).GetMethod("Add", new[] { typeof(string), typeof(Type) }),
                headerNameParam, headerTypeParam);

            headerExpressions.AddRange(new Expression[] {
                                           headerNameParam,
                                           headerNameAssign,
                                           headerTypeParam,
                                           headerTypeAssign,
                                           columnsAddMethod,
                                       });

            var indexerProp = typeof(DataRow).GetProperty("Item", new[] { typeof(string) });
            var indexerParam = Expression.Property(newRowParam, indexerProp, Expression.Constant(name, typeof(string)));
            var propertyReaderMethod = Expression.Call(loopVariable, getMethod);
            var assign = Expression.Assign(indexerParam, Expression.TypeAs(propertyReaderMethod, typeof(object)));

            bodyExpressions.AddRange(new Expression[] { indexerParam, propertyReaderMethod, assign });
        }


        var finalExpressions = new List<Expression>() {
            tableParam,
            rowsParam,
            loopVariable,
            columnsVariable,
            columnsAssign,
            newRowParam,
            newRowAssign,
        };
        finalExpressions.AddRange(headerExpressions);

        var loop = ExpressionHelper.ForEach(rowsParam, loopVariable, Expression.Block(bodyExpressions));
        finalExpressions.Add(loop);
        var compilable = Expression.Block(finalExpressions);
        var code = compilable.ToString();
        Trace.WriteLine(code);
        var compiled = Expression.Lambda<Action<DataTable, IEnumerable<T>>>(compilable, tableParam, rowsParam).Compile();
        return compiled;
    }

But, when I call .Compile() method (at the end of block, just before return) I get this error:

An exception of type 'System.InvalidOperationException' occurred in System.Core.dll but was not handled in user code

Additional information: variable 'item' of type 'TestEntity' referenced from scope '', but it is not defined

Do you have any idea what I missed here? Thanks in advance. Cheers.

UPDATE: Here is the loop generator:

public static class ExpressionHelper {

    public static Expression ForEach(Expression collection, ParameterExpression loopVar, Expression loopContent) {

        var elementType = loopVar.Type;
        var enumerableType = typeof(IEnumerable<>).MakeGenericType(elementType);
        var enumeratorType = typeof(IEnumerator<>).MakeGenericType(elementType);

        var enumeratorVar = Expression.Variable(enumeratorType, "enumerator");
        var getEnumeratorCall = Expression.Call(collection, enumerableType.GetMethod("GetEnumerator"));
        var enumeratorAssign = Expression.Assign(enumeratorVar, getEnumeratorCall);

        // The MoveNext method's actually on IEnumerator, not IEnumerator<T>
        var moveNextCall = Expression.Call(enumeratorVar, typeof(IEnumerator).GetMethod("MoveNext"));

        var breakLabel = Expression.Label("LoopBreak");

        var loop = Expression.Block(new[] { enumeratorVar },
            enumeratorAssign,
            Expression.Loop(
                Expression.IfThenElse(
                    Expression.Equal(moveNextCall, Expression.Constant(true)),
                    Expression.Block(new[] { loopVar },
                        Expression.Assign(loopVar, Expression.Property(enumeratorVar, "Current")),
                        loopContent
                    ),
                    Expression.Break(breakLabel)
                ),
            breakLabel)
        );

        return loop;
    }

}
2
  • loopVariable is defined as a parameter, but you never provide it to your lambda. Did you mean to use Expression.Variable for it? Commented Dec 10, 2016 at 10:42
  • @Rob I changed the loopVariable to Expression.Variable but the problem exists. Commented Dec 10, 2016 at 11:48

2 Answers 2

1

Updated working code is below and working example in DotNetFiddle is - https://dotnetfiddle.net/fyMOxe

Originally your code has two following issues:

  1. Body expression should have separated variables and actual expressions. In your sample you are adding ExpressionParameter within other parameters and pass them to Body call. But they should be passed standalone. So you have to pass first parameter with list of variables, and second one with actual expressions.

  2. Your loop code missed actual expression var dr = dataTable.NewRow(); that you generated, but didn't add to loop. And also it missed last call with dataTable.Rows.Add(dr); as filled row needs to be added back to Rows.

In in my example i fixed these two issues and now code fills DataTable based on list of Test entities.

public class Program
{
    static void Main(string[] args)
    {

        var data = new List<Test>()
        {
            new Test() {ID = 1, Name = "1Text"},
            new Test() {ID = 2, Name = "2Text"},
        };

        var action = ExpressionHelper.GetAction<Test>();

        var dataTable = new DataTable();
        action(dataTable, data);

        foreach (DataRow row in dataTable.Rows)
        {
            Console.WriteLine($"ID: {row["ID"]}, Name: {row["Name"]}");
        }

    }

}

public class ExpressionHelper
{
    public static Action<DataTable, IEnumerable<T>> GetAction<T>()
    {
        //if (_filler != null)
        //  return null;
        var type = typeof(T);
        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        var tableParam = Expression.Parameter(typeof(DataTable), "targetTable");
        var rowsParam = Expression.Parameter(typeof(IEnumerable<T>), "rows");

        var loopVariable = Expression.Parameter(typeof(T), "item");

        var columnsVariable = Expression.Parameter(typeof(DataColumnCollection), "columns");
        var columnsAssign = Expression.Assign(columnsVariable,
            Expression.Property(tableParam, typeof(DataTable).GetProperty("Columns")));


        var headerExpressions = new List<Expression>();
        var bodyExpressions = new List<Expression>();

        var headerNameParam = Expression.Parameter(typeof(string), "columnName");
        var headerTypeParam = Expression.Parameter(typeof(Type), "columnType");

        var newRowParam = Expression.Parameter(typeof(DataRow), "currentRow");
        var newRowAssign = Expression.Assign(newRowParam, Expression.Call(tableParam, typeof(DataTable).GetMethod("NewRow")));

        bodyExpressions.Add(newRowAssign);
        foreach (var prop in props)
        {
            var getMethod = prop.GetGetMethod(false);
            if (getMethod == null)
                continue;
            var attr = prop.GetCustomAttribute<UdtColumnAttribute>();
            var name = attr == null ? prop.Name : attr.ColumnName;

            var headerNameAssign = Expression.Assign(headerNameParam, Expression.Constant(name, typeof(string)));

            var headerTypeAssign = Expression.Assign(headerTypeParam, Expression.Constant(prop.PropertyType, typeof(Type)));

            var columnsAddMethod = Expression.Call(columnsVariable,
                typeof(DataColumnCollection).GetMethod("Add", new[] { typeof(string), typeof(Type) }),
                headerNameParam, headerTypeParam);

            headerExpressions.AddRange(new Expression[] {
                                       headerNameAssign,
                                       headerTypeAssign,
                                       columnsAddMethod,
                                   });

            var indexerProp = typeof(DataRow).GetProperty("Item", new[] { typeof(string) });
            var indexerParam = Expression.Property(newRowParam, indexerProp, Expression.Constant(name, typeof(string)));
            var propertyReaderMethod = Expression.Call(loopVariable, getMethod);
            var assign = Expression.Assign(indexerParam, Expression.TypeAs(propertyReaderMethod, typeof(object)));

            bodyExpressions.AddRange(new Expression[] { assign });
        }

        // we should add that row back to collection
        var addRow = Expression.Call(
            Expression.Property(tableParam, "Rows"),
            typeof(DataRowCollection).GetMethod("Add", new Type[] {typeof(DataRow)}),
            newRowParam);
        bodyExpressions.Add(addRow);


        var finalExpressions = new List<Expression>()
        {
            columnsAssign,
            newRowAssign,
        };

        var variables = new List<ParameterExpression>()
        {
            loopVariable,
            columnsVariable,
            newRowParam,
            headerNameParam,
            headerTypeParam
        };

        finalExpressions.AddRange(headerExpressions);

        var loop = ExpressionHelper.ForEach(rowsParam, loopVariable, Expression.Block(bodyExpressions));
        finalExpressions.Add(loop);
        var compilable = Expression.Block(variables, finalExpressions);
        var code = compilable.ToString();
        Trace.WriteLine(code);
        var compiled = Expression.Lambda<Action<DataTable, IEnumerable<T>>>(compilable, tableParam, rowsParam).Compile();
        return compiled;
    }


    public static Expression ForEach(Expression collection, ParameterExpression loopVar, Expression loopContent)
    {

        var elementType = loopVar.Type;
        var enumerableType = typeof(IEnumerable<>).MakeGenericType(elementType);
        var enumeratorType = typeof(IEnumerator<>).MakeGenericType(elementType);

        var enumeratorVar = Expression.Variable(enumeratorType, "enumerator");
        var getEnumeratorCall = Expression.Call(collection, enumerableType.GetMethod("GetEnumerator"));
        var enumeratorAssign = Expression.Assign(enumeratorVar, getEnumeratorCall);

        // The MoveNext method's actually on IEnumerator, not IEnumerator<T>
        var moveNextCall = Expression.Call(enumeratorVar, typeof(IEnumerator).GetMethod("MoveNext"));

        var breakLabel = Expression.Label("LoopBreak");

        var loop = Expression.Block(new[] { enumeratorVar },
            enumeratorAssign,
            Expression.Loop(
                Expression.IfThenElse(
                    Expression.Equal(moveNextCall, Expression.Constant(true)),
                    Expression.Block(new[] { loopVar },
                        Expression.Assign(loopVar, Expression.Property(enumeratorVar, "Current")),
                        loopContent
                    ),
                    Expression.Break(breakLabel)
                ),
            breakLabel)
        );

        return loop;
    }
}

public class Test
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class UdtColumnAttribute : Attribute
{
    public string ColumnName { get; set; }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks bro. I figured out the first issue and was able to compile the expression; but invoking the compiled expression, was throwing null reference exception - bcuz of the second issue - which I was trying to fix. You saved my day dude. Thank you again.
Glad that i helped :)
0

I am unsure what is the reason for using expression trees here, I guess you are worried about performance if using reflection (since you will not send this expression to EF, or any other queryProvider, right)? That also means that you forgot to add the _filler = compiled; just before returning....

The main problem with expression trees when compiled to delegates is, that they are not refactor-friendly at all, and hard to read/understand.

Now if you would use directly reflection, than the only penalty is the getter calls on the objects, all the rest of the work you have to do anyways. So you can cache that part, and then do all the rest without all the complexity, have a much cleaner code and more readable.

public class PropHelper
{
    public PropertyInfo PropInfo {get;set;}
    public Func<object, object> Getter {get;set;}
}

private static readonly ConcurrentDictionary<Type, IEnumerable<PropHelper>> s_cachedPropHelpers = new ConcurrentDictionary<Type, IEnumerable<PropHelper>>();

public static IEnumerable<PropHelper> GetPropHelpers(Type type)
{
    return s_cachedPropHelpers.GetOrAdd(type, t => 
        {
            var props = t.GetProperties();
            var result = new List<PropHelper>();
            var parameter = Expression.Parameter(typeof(object));
            foreach(var prop in props)
            {
                result.Add(new PropHelper
                    {
                        PropInfo = prop,
                        Getter = Expression.Lambda<Func<object, object>>(
                            Expression.Convert(
                                Expression.MakeMemberAccess(
                                    Expression.Convert(parameter, t),
                                    prop), 
                                typeof(object)),
                            parameter).Compile(),
                    });
            }
            return result;
        });
}

private static Action<DataTable, IEnumerable<T>> GetAction<T>() 
{
    return (dataTable, list) => {
        var props = GetPropHelpers(typeof(T));

        foreach(var prop in props)
            dataTable.Columns.Add(prop.PropInfo.Name, prop.PropInfo.PropertyType);

        foreach (var item in list) 
        {
            var dr = dataTable.NewRow();
            foreach(var prop in props)
                dr[prop.PropInfo.Name] = prop.Getter(item);
            dataTable.Rows.Add(dr);
        }
    };
}

Is that not easier to read?

Comments

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.