在.NET 4.5中,最简单的动态生成代码方式是什么?

7
我正在编写一种特定类型的对象映射器。基本上,我想从一个具有字段abcDataTable转换为具有属性abc的对象(该对象的类将手动编写)。将会有许多不同的DataTable和许多不同的类需要进行映射,因此我希望创建一个通用机制来执行此数据复制。基本上,我希望得到以下函数:
public T Map<T>(DataTable t) where T: new() { ... }

现在,我可以使用反射来实现此功能,但这会很慢。该函数将成为框架的核心,并且经常使用。因此,我正在考虑动态代码生成。第一次在特定类型 T 上运行此方法时,它将执行必要的反射并发出一个匿名方法,该方法执行所有适当的映射。下一次它将只运行该代码。这应该是尽可能高效的。
只有一个问题-我从未在运行时发出过代码。我该怎么做呢?我看了一下表达式,但它们只能执行表达式,而不能执行一系列语句。
然后有 CodeDOM 和 CSharpCodeProvider。那种方法有点起作用-我可以生成 C# 代码作为字符串,在运行时编译它,然后获得引用。但是,它涉及 C# 编译器并生成一个全新的内存中程序集。对于一个简单的方法来说,听起来有点...重量级。
有更简单的方法吗?是否有生成轻量级、匿名方法而不附加到任何程序集(或附加到现有程序集)的方法?
好的,既然有人要求示例。
以下是手写的类。
class MyBusinessObject
{
    public int a;
    public string b { get; set; }
}

这是一个手动准备的DataTable(在实际应用中,通常会来自外部库):
DataTable t = new DataTable();
t.AddColumn("a", typeof(int));
t.AddColumn("b", typeof(string));
t.AddRow(42, "Meaning");

这里是应该动态生成的方法:
(DataRow drow, MyBusinessObject o) =>
{
    o.a = (int)drow["a"];
    o.b = (string)drow["b"];
}

出于简洁起见,我省略了一些其他的内容,但这是问题的核心。


答案很大程度上取决于您想要实现什么。提供一个“执行所有适当映射”的具体方法的例子将非常有帮助。 - Sergey Kalinichenko
或许AutoMapper可以解决这个问题? - leppie
4个回答

8
在.NET 3.5+中动态生成代码的最简单方法是通过将LINQ表达式树转换为可执行代码,通过LambdaExpression类的Compile方法。.NET 4.0大大扩展了可能性,支持超出.NET 3.5简单表达式的代码结构,让您构建完全功能的方法。生成的代码给您提供了与常规编译代码相同的高性能,假设您的表达式生成器在生成代码时应用了与C#编译器相同类型的优化。以下是如何从片段生成代码的方法:
// 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,以按需执行分配操作。

2

不要轻易忽略表达式,你可以使用不同的方式来实现你的目标。

  1. If you are using .NET 4+, you the expression trees have been extended to support blocks of code. While you can't use this functionality with the lambda syntactic sugar, you can use the Expression.Block method to create a code block.

  2. Use a constructor that has a parameter for each field you are mapping. The generated code could mimic return new T(ExtractA(t), ExtractB(t), ...). In this case you would remove the where T : new() constraint from Map<T> and instead rely on your object model classes having a constructor that can be found using reflection with a parameter for each mapped property.

  3. Use helper methods to execute a series of statements as if it were a single statement. The generated code could mimic return ApplyProperties(t, new T(), new[] { applyA, applyB, ... }), where applyA and applyB are Action<DataTable, T> delegates that were separately compiled from expressions designed to set a single specific property. The ApplyProperties method is a helper method in your code like the following:

     private T ApplyProperties<T>(DataTable t, T result, Action<DataTable, T>[] setters)
     {
         foreach (var action in setters)
         {
             action(t, result);
         }
    
         return result;
     }
    

不错,它还有其他一些在Linq中不允许的东西(比如赋值)!我现在正在检查它,看起来这是真正的东西! - Vilx-
@Vilx- 在LINQ表达式中允许使用赋值语句,循环语句,条件语句等等。 - Sergey Kalinichenko
这段代码无法编译:Expression<Action<DataRow, Test>> fun = (d, o) => o.Member1 = (int)d["a"]; - Vilx-
哦,我想我打错了。我是指“lambda”,而不是“linq”。对不起。 - Vilx-

2
有时候使用第三方库是最容易的方法,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"]));

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

我已经在生产环境中使用过这个,它的表现很好。


有趣。我了解到它为每个成员创建一个访问器函数,但是每次执行映射操作时,我仍然需要进行某种成员枚举(即使只是成员名称的缓存列表)。我已经使用表达式创建了所需的内容,而没有进行这样的枚举。 :) - Vilx-

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接