7

I'm writing a specific kind of object-mapper. Basically I want to convert from a DataTable that has the fields a, b and c to an object that has properties a, b and c (the class of the object will be written by hand). There will be many different DataTables and many different classes that they will need to map to, so I want to make a generic mechanism that performs this data copying. Basically, I want the following function:

public T Map<T>(DataTable t) where T: new() { ... }

Now, I can do this with Reflection, but that's slow. This function will be at the core of the framework and will be used often. So I'm thinking about dynamic code generation. The first time this method is run on a specific T it will perform the necessary reflection and emit an anonymous method that does all the appropriate mapping. The next time it will just run that code. This should be as performant as possible.

Just one problem - I've never emitted code at runtime. How do I do that? I looked at Expressions, but they can only do, well, expressions, not a series of statements.

Then there's CodeDOM and CSharpCodeProvider. That sort of works - I can generate C# code as a string, compile it on-the-fly and then get a reference. However it involves the C# compiler and generates a whole new in-memory assembly. Sounds a bit... heavyweight for one simple method.

Is there some simpler way? Something that generates a lightweight, anonymous method not attached to any assembly (or attached to an existing assembly)?


OK, since people asked for an example.

Here's a class, written by hand

class MyBusinessObject
{
    public int a;
    public string b { get; set; }
}

Here's a DataTable, prepared by hand (in real life, this will come from an external library):

DataTable t = new DataTable();
t.AddColumn("a", typeof(int));
t.AddColumn("b", typeof(string));
t.AddRow(42, "Meaning");

Here's the method that should be generated on the fly:

(DataRow drow, MyBusinessObject o) =>
{
    o.a = (int)drow["a"];
    o.b = (string)drow["b"];
}

I've omitted some other stuff I need for brevity, but that's the meat of the issue.

4

4 回答 4

8

在 .NET 3.5+ 中动态生成代码的最简单方法是通过类的方法将LINQ 表达式树转换为可执行代码。.NET 4.0 极大地扩展了可能性,在 .NET 3.5 的简单表达式之外添加了对代码结构的支持,让您可以构建功能齐全的方法。假设您的表达式生成器在生成代码时应用了与 C# 编译器相同的优化类型,则生成的代码为您提供与定期编译的代码相同的高性能。CompileLambdaExpression

以下是从代码段生成代码的方法:

// nameToProperty is a dictionary with keys representing string parameters
// that you pass to drow's indexer, and values representing names of properties
// or fields of the target object.
private static Action<DataRow,T> MakeGetter<T>(IDictionary<string,string> nameToProperty) {
    var sequence = new List<Expression>();
    var drowParam = Expression.Parameter(typeof(DataRow));
    var oParam = Expression.Parameter(typeof(T));
    var indexer = typeof(DataRow)
        .GetDefaultMembers()
        .OfType<PropertyInfo>()
        .Where(pinf => pinf.GetIndexParameters().Length == 1
               &&      pinf.GetIndexParameters()[0].ParameterType == typeof(string))
        .Single();
    foreach (var pair in nameToProperty) {
        var indexExpr = Expression.Property(
            drowParam
        ,   indexer
        ,   Expression.Constant(pair.Key));
        sequence.Add(Expression.Assign(
            Expression.PropertyOrField(pair.Value)
        ,   indexExpr
        ));
    }
    return (Action<DataRow,T>)Expression.Lambda(
        Expression.Block(sequence)
    ,   drowParam
    ,   oParam
    ).Compile();
}

使用此方法,您应该能够生成编译Action的 s,可以根据需要进行分配。

于 2013-07-30T14:11:57.783 回答
2

派对迟到了,但 Marc Gravell 有一个很好的实用程序,叫做FastMember。使用 FastMember,您可以从 DataTable 映射到对象,甚至是动态对象。

var accessor = TypeAccessor.Create(type);
string propName = // something known only at runtime

while( /* some loop of data */ ) {
   accessor[obj, propName] = rowValue;
}

我已经在生产中使用了它,它表现得很好。

于 2013-08-22T18:23:00.717 回答
2

我不会这么快就拒绝表达。您可以通过多种不同方式之一使用表达式来实现您的目标。

  1. 如果您使用的是 .NET 4+,则表达式树已扩展为支持代码块。虽然您不能将此功能与 lambda 语法糖一起使用,但您可以使用该Expression.Block方法来创建代码块。

  2. 使用一个构造函数,该构造函数对要映射的每个字段都有一个参数。生成的代码可以模仿return new T(ExtractA(t), ExtractB(t), ...). 在这种情况下,您将从中删除where T : new()约束Map<T>,而是依赖于具有构造函数的对象模型类,该构造函数可以使用反射找到每个映射属性的参数。

  3. 使用辅助方法来执行一系列语句,就好像它是一条语句一样。生成的代码可以模仿return ApplyProperties(t, new T(), new[] { applyA, applyB, ... }), whereapplyAapplyBareAction<DataTable, T>委托,这些委托是从旨在设置单个特定属性的表达式中单独编译的。该ApplyProperties方法是代码中的辅助方法,如下所示:

     private T ApplyProperties<T>(DataTable t, T result, Action<DataTable, T>[] setters)
     {
         foreach (var action in setters)
         {
             action(t, result);
         }
    
         return result;
     }
    
于 2013-07-30T14:15:31.323 回答
2

有时使用 3rd 方库是最简单的方法,AutoMapper只需几行代码即可完成您想要的操作

//This just needs to be run once, maybe in a static constructor somewhere.
Mapper.CreateMap<IDataReader, MyBusinessObject>();



//This line does your mapping.
List<MyBusinessObject> myBusinessObject = 
    Mapper.Map<IDataReader, List<MyBusinessObject>>(myDataTable.CreateDataReader());

如果您的源数据与您的业务对象不完全匹配,您只需将一些设置信息添加到CreateMap.

class MyBusinessObject
{
    public int Answer;
    public string Question { get; set; }
}

//In some static constructor somewhere, this maps "a" to "Answer" and "b" to "Question".
Mapper.CreateMap<IDataReader, MyBusinessObject>()
      .ForMember(dto => dto.Answer, opt => opt.MapFrom(rdr => rdr["a"]))
      .ForMember(dto => dto.Question, opt => opt.MapFrom(rdr => rdr["b"]));
于 2013-07-30T14:34:58.067 回答