Search code examples
c#c#-4.0lambdaexpression-trees

Writing an object's property to a DataTable through Expression tree


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;
    }

}

Solution

  • 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; }
    }