【问题标题】:C# using ExpressionTree to map DataTable to List<T>C# 使用 ExpressionTree 将 DataTable 映射到 List<T>
【发布时间】:2019-02-01 08:50:19
【问题描述】:

我编写了一个ToList(); 扩展方法来将DataTable 转换为List。这仅在某些情况下有效,但我们有很多使用 DataTables 的旧代码,有时需要它。我的问题是这种方法适用于反射什么是好的但不是那么高性能。对于 100.000 个 DataRows,我需要大约 1.2sek。

所以我决定用表达式树来构建它。起初我想替换属性的 Setter 调用。到目前为止,我可以轻松获得价值:

var exactType = Nullable.GetUnderlyingType(propType) ?? propType;
var wert = Convert.ChangeType(zeile[spaltenname], exactType);

并设置它:

propertyInfo.SetValue(tempObjekt, wert, null);

现在我搜索了 StackOverflow 并找到了这个:

var zielExp = Expression.Parameter(typeof(T));
var wertExp = Expression.Parameter(propType);

var propertyExp = Expression.Property(zielExp, matchProp);
var zuweisungExp = Expression.Assign(propertyExp, wertExp);

var setter = Expression.Lambda<Action<T, int>>(zuweisungExp, zielExp, wertExp).Compile();
setter(tempObjekt, wert);

我的大问题是 Lambda 操作需要一个整数。但我需要这个期待我的财产的类型。我通过 PropertyInfo 获得了我的财产类型。但不能让这个工作。以为我可以轻松做到:

Action<T, object>

但这会导致以下异常:

ArgumentException 来自“System.Int32”类型的 ParameterExpression 不能用作类型“System.Object”的委托参数。

有人知道可能的解决方案吗?

【问题讨论】:

  • 所以你试图根据传入的值调用对象的设置器?属性的类型是 int 吗?我有点困惑。您可以在没有动态调用的情况下做您想做的事。但为了帮助我需要知道它是否是单个属性、所有属性、属性列表等。您可以使用一个操作,您可以获取属性的 GetSetMethod() 的方法信息,并且可以将类型传递为一个通用参数,或者由于您不知道它,您将使用对象并调用 Expression.Convert。

标签: c# lambda datatable expression


【解决方案1】:

您可以使用this overload 来代替通用的Expression.Lambda 方法,它采用类型:

public static LambdaExpression Lambda(
   Type delegateType,
   Expression body,
   params ParameterExpression[] parameters
)

然后您可以使用Type.MakeGenericType 方法为您的操作创建类型:

var actionType = typeof(Action<,>).MakeGenericType(typeof(T), proptype);
var setter = Expression.Lambda(actionType, zuweisungExp, zielExp, wertExp).Compile();

按照有关性能的 cmets 进行编辑:

您也可以只构建表达式运行时以通过选择将DataTable 映射到您的T 类型的类,因此只需要使用一次反射,这将大大提高性能。我编写了以下扩展方法来将DataTable 转换为List&lt;T&gt;(请注意,如果您不打算将所有数据列映射到类中的属性,此方法抛出运行时异常,因此,如果可能发生这种情况,请务必注意这一点):

public static class LocalExtensions
{
    public static List<T> DataTableToList<T>(this DataTable table) where T : class
    {
        //Map the properties in a dictionary by name for easy access
        var propertiesByName = typeof(T)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .ToDictionary(p => p.Name);
        var columnNames = table.Columns.Cast<DataColumn>().Select(dc => dc.ColumnName);

        //The indexer property to access DataRow["columnName"] is called "Item"
        var property = typeof(DataRow).GetProperties().First(p => p.Name == "Item" 
            && p.GetIndexParameters().Length == 1 
            && p.GetIndexParameters()[0].ParameterType == typeof(string));

        var paramExpr = Expression.Parameter(typeof(DataRow), "r");
        var newExpr = Expression.New(typeof(T));

        //Create the expressions to map properties from your class to the corresponding
        //value in the datarow. This will throw a runtime exception if your class 
        //doesn't contain properties for all columnnames!
        var memberBindings = columnNames.Select(columnName =>
        {
            var pi = propertiesByName[columnName];
            var indexExpr = Expression.MakeIndex(paramExpr, property, 
                new[] { Expression.Constant(columnName) });
            //Datarow["columnName"] is of type object, cast to the right type
            var convert = Expression.Convert(indexExpr, pi.PropertyType);

            return Expression.Bind(pi, convert);
        });
        var initExpr = Expression.MemberInit(newExpr, memberBindings);
        var func = Expression.Lambda<Func<DataRow, T>>(initExpr,paramExpr).Compile();

        return table.Rows.Cast<DataRow>().Select(func).ToList();
    }
}

然后我编写了一个小测试类和一些代码,这些代码创建了一个包含 1,000,000 行的数据表,这些行映射到一个列表。在我的电脑上构建表达式 + 转换为列表现在只需要 486 毫秒(当然这是一个非常小的类):

class Test
{
    public string TestString { get; set; }
    public int TestInt { get; set; }
}

class Program
{
    static void Main()
    {
        DataTable table = new DataTable();
        table.Columns.Add(new DataColumn("TestString", typeof(string)));
        table.Columns.Add(new DataColumn("TestInt", typeof(int)));

        for(int i = 0; i < 1000000; i++)
        {
            var row = table.NewRow();
            row["TestString"] = $"String number: {i}";
            row["TestInt"] = i;
            table.Rows.Add(row);
        }

        var stopwatch = Stopwatch.StartNew();

        var myList = table.DataTableToList<Test>();

        stopwatch.Stop();
        Console.WriteLine(stopwatch.Elapsed.ToString());
    }
}

【讨论】:

  • 谢谢,这看起来不错。但是,如果我尝试调用 setter,编译器会告诉我“预期的方法或委托”。我正在尝试这个 setter(myTempObject, wert);
  • @Sebi 你需要拨打setter.DynamicInvoke(...)才能使用它
  • 感谢这项工作。 LoadTime 现在是 80sek :D 但这是……。我得到修复。
  • @Sebi DynamicInvoke 需要使用大量反射,所以我可以想象如果你在一个紧密的循环中调用它会很慢;-) 但我仍然不完全理解你在尝试什么要做到这一点,您想要一个通用方法来将数据表映射到现有类的列表?
  • @Sebi 编辑了我的答案,我认为这就是您要寻找的(反射用于构建表达式,而不是在循环中)
【解决方案2】:

我想我对你的理解是正确的。我无法翻译你的变量,所以我在这里根据我在你的问题中看到的最好的猜测:

对于Action&lt;object,object&gt;,第一个参数是实体本身,第二个参数是您可以使用的属性类型

var instance = Expression.Parameter(typeof(object), "i");
var argument = Expression.Parameter(typeof(object), "a");
var convertObj = Expression.TypeAs(instance, propertyInfo.DeclaringType);
var convert = Expression.Convert(argument, propertyInfo.PropertyType);
var setterCall = Expression.Call(convertObj, propertyInfo.GetSetMethod(), convert);
var compiled = ((Expression<Action<object, object>>) Expression.Lambda(setterCall, instance, argument)).Compile();

如果您知道T(即实体的类型),则可以改为:

var instance = Expression.Parameter(typeof(T), "i");
var argument = Expression.Parameter(typeof(object), "a");
var convert = Expression.Convert(argument, propertyInfo.PropertyType);
var setterCall = Expression.Call(instance , propertyInfo.GetSetMethod(), convert);
var compiled = ((Expression<Action<T, object>>) Expression.Lambda(setterCall, instance, argument)).Compile();

【讨论】:

  • 问题是每个解决方案都需要使用反射。所以这不是更快,然后仍然调用 propertyInfo.SetValue();
  • 如果你缓存 lambda 并且只编译一次它应该会更快。你不应该在每个循环中调用它。您的另一个选择是使用 expression.property 并像上面一样分配,但您仍然必须转换为正确的类型,因为您仍然需要知道您需要属性信息的类型
  • 是的,我构建了一个缓存所有内容的版本,但仍然比反射访问慢 2 倍。所以我认为使用它对我来说更好。感谢您的帮助。
  • 上面的答案已经更新,他的解决方案现在应该很适合你。我在代码中使用类似的东西将数据读取器记录映射到对象
  • 哦,太好了,我会试一试:)
【解决方案3】:

我在这里发表评论是因为我没有必要的声誉来评论@Alexander Derek 的回复

    var memberBindings = columnNames.Select(columnName =>
    {
        var pi = propertiesByName[columnName];
        var indexExpr = Expression.MakeIndex(paramExpr, property, 
            new[] { Expression.Constant(columnName) });
        //Datarow["columnName"] is of type object, cast to the right type
        var convert = Expression.Convert(indexExpr, pi.PropertyType);

        return Expression.Bind(pi, convert);
    });

为了避免运行时异常,我添加了 try-catch 和 .where()

        var memberBindings = columnNames.Select(columnName =>
        {
            try
            {
                var pi = propertiesByName[columnName];
                var indexExpr = Expression.MakeIndex(paramExpr, property,
                    new[] { Expression.Constant(columnName) });
                var convert = Expression.Convert(indexExpr, pi.PropertyType);
                return Expression.Bind(pi, convert);
            }
            catch(Exception e)
            {
                return null;
            }                
        });
        var initExpr = Expression.MemberInit(newExpr, memberBindings.Where(obj => obj != null));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-05-26
    • 1970-01-01
    • 2016-07-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-26
    • 1970-01-01
    相关资源
    最近更新 更多