【问题标题】:Writing an object's property to a DataTable through Expression tree通过表达式树将对象的属性写入 DataTable
【发布时间】:2026-01-23 09:40:01
【问题描述】:

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

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

作为var list = new List<Test>() { /* some items here */ };。我正在通过这个 sn-p 从list 生成一个DataTable

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

现在我正在尝试生成一些表达式树以在运行时执行上述 sn-p(以通用方式)。然而,我的尝试让我来到了这里:

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

}

【问题讨论】:

  • loopVariable 被定义为一个参数,但你永远不会将它提供给你的 lambda。您的意思是使用Expression.Variable 吗?
  • @Rob 我把loopVariable改成了Expression.Variable,但是问题还是存在的。

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


【解决方案1】:

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

原来你的代码有以下两个问题:

  1. 正文表达式应该有分开的变量和实际表达式。在您的示例中,您将在其他参数中添加 ExpressionParameter 并将它们传递给 Body 调用。但它们应该独立通过。因此,您必须将第一个参数与变量列表一起传递,第二个参数与实际表达式一起传递。

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

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

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

【讨论】:

  • 谢谢兄弟。我想出了第一个问题并能够编译表达式;但是调用已编译的表达式,却抛出了null reference exception - 第二个问题的bcuz - 我试图解决这个问题。你救了我的一天,伙计。再次感谢您。
  • 很高兴我能帮上忙 :)
【解决方案2】:

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

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

现在,如果您直接使用反射,那么唯一的惩罚是对象上的 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);
        }
    };
}

这不是更容易阅读吗?

【讨论】: