【问题标题】:What's the easiest way to generate code dynamically in .NET 4.5?在 .NET 4.5 中动态生成代码的最简单方法是什么?
【发布时间】:2013-07-30 17:29:29
【问题描述】:

我正在编写一种特定类型的对象映射器。基本上我想从具有abc 字段的DataTable 转换为具有abc 属性的对象(对象的类将是手写)。将有许多不同的 DataTables 和许多不同的类需要映射到,因此我想创建一个通用机制来执行此数据复制。基本上,我想要以下功能:

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

现在,我可以使用反射来做到这一点,但这很慢。该功能将是框架的核心,并且会经常使用。所以我正在考虑动态代码生成。第一次在特定的T 上运行此方法时,它将执行必要的反射并发出一个匿名方法,该方法执行所有适当的映射。下一次它将只运行该代码。这应该尽可能高效。

只有一个问题 - 我从未在运行时发出代码。我怎么做?我看了Expressions,但他们只能做,嗯,表达式,而不是一系列语句。

然后是 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"];
}

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

【问题讨论】:

  • 答案很大程度上取决于您要达到的目标。提供一个“进行所有适当映射”的特定方法的示例将有很大帮助。
  • AutoMapper 可以解决这个问题吗?

标签: .net code-generation


【解决方案1】:

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

您可以通过以下方式从您的 sn-p 生成代码:

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

使用此方法,您应该能够生成编译后的Actions,它会根据需要进行分配。

【讨论】:

    【解决方案2】:

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

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

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

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

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

    【讨论】:

    • 很好,它还有其他在 Linq 中不允许的东西(比如赋值)!我现在正在检查它,看起来这是真的!
    • @Vilx- 在 LINQ 表达式中非常允许赋值;循环、条件等也是如此。
    • 这不能编译:Expression&lt;Action&lt;DataRow, Test&gt;&gt; fun = (d, o) =&gt; o.Member1 = (int)d["a"];
    • 哦,我想我打错了。我的意思是“lambda”,而不是“linq”。我很抱歉。
    【解决方案3】:

    有时使用 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"]));
    

    【讨论】:

      【解决方案4】:

      聚会迟到了,但 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;
      }
      

      我在生产中使用过它,它的性能很好。

      【讨论】:

      • 有趣。我认为它为每个成员创建了一个访问器函数,并且每次执行映射操作时我仍然需要进行某种成员枚举(即使它只是成员名称的缓存列表)。我已经用表达式创建了我需要的东西,并且没有这样的枚举。 :)
      猜你喜欢
      • 1970-01-01
      • 2014-10-19
      • 1970-01-01
      • 1970-01-01
      • 2010-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多