通过表达式树

时间:2016-12-10 10:34:18

标签: c# c#-4.0 lambda expression-trees

我有一个这个简单模型的列表:

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

var list = new List<Test>() { /* some items here */ };。我正在通过此代码段从DataTable生成list

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

现在我正在尝试生成一些表达式树,以便在运行时执行上述代码段(以通用方式)。但是,我的尝试让我来到这里:

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

但是,当我调用.Compile()方法时(在块的末尾,就在return之前),我收到此错误:

  

发生了'System.InvalidOperationException'类型的异常   System.Core.dll但未在用户代码中处理

     

附加信息:类型的变量'item'   引用的'TestEntity'   范围'',但未定义

你知道我错过了什么吗?提前致谢。欢呼声。

更新 这是循环生成器:

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 个答案:

答案 0 :(得分:1)

下面更新了工作代码,DotNetFiddle中的工作示例是 - https://dotnetfiddle.net/fyMOxe

最初您的代码有以下两个问题:

  1. 正文表达式应该包含已分离的变量和实际表达式。在您的示例中,您要在其他参数中添加ExpressionParameter并将其传递给Body来电。但它们应该是独立传递的。所以你必须传递第一个参数和变量列表,第二个参数传递实际表达式。

  2. 您的循环代码错过了您生成的实际表达式var dr = dataTable.NewRow();,但未添加到循环中。并且它也错过了与dataTable.Rows.Add(dr);的最后一次通话,因为需要将已填充的行添加回行。

  3. 在我的示例中,我修复了这两个问题,现在根据DataTable实体列表填写Test代码。

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

答案 1 :(得分:0)

我不确定在这里使用表达式树的原因是什么,我猜你在使用反射时担心性能(因为你不会将这个表达式发送到EF,或者任何其他的queryProvider,对吧)?这也意味着你忘了添加_filler =编译;就在回来之前......

编译为委托时表达式树的主要问题是,它们根本不是重构友好的,并且难以阅读/理解。

现在,如果您使用直接反射,那么唯一的惩罚就是getter调用对象,所有其他工作都必须要做。因此,您可以缓存该部分,然后在没有所有复杂性的情况下完成其余部分,使代码更清晰,更具可读性。

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

这不容易阅读吗?