我正在编写一种特定的对象映射器。基本上我想将具有字段DataTable
,a
和b
的{{1}}转换为具有属性c
,a
的对象和b
(对象的类将手写)。将有许多不同的DataTable和许多不同的类需要映射到,所以我想制作一个执行此数据复制的通用机制。基本上,我想要以下功能:
c
现在,我可以用Reflection做到这一点,但这很慢。该功能将成为框架的核心,并将经常使用。所以我在考虑动态代码生成。第一次在特定的public T Map<T>(DataTable t) where T: new() { ... }
上运行此方法时,它将执行必要的反射并发出一个匿名方法,该方法执行所有适当的映射。下次它将运行该代码。这应尽可能高效。
只有一个问题 - 我从未在运行时发出代码。我怎么做?我看了T
,但他们只能表达,而不是一系列陈述。
然后是CodeDOM和Expressions
。那种工作 - 我可以生成C#代码作为字符串,在运行中编译它然后获得引用。但它涉及C#编译器并生成一个全新的内存中程序集。对于一个简单的方法,听起来有点......重量级。
有一些更简单的方法吗?生成轻量级匿名方法的东西没有附加到任何程序集(或附加到现有程序集)?
好的,因为人们要求一个例子。
这是一个手写的课程
CSharpCodeProvider
这是一个手工准备的DataTable(在现实生活中,它将来自外部库):
class MyBusinessObject
{
public int a;
public string b { get; set; }
}
以下是应该即时生成的方法:
DataTable t = new DataTable();
t.AddColumn("a", typeof(int));
t.AddColumn("b", typeof(string));
t.AddRow(42, "Meaning");
为了简洁,我省略了其他一些我需要的东西,但这是问题的关键。
答案 0 :(得分:7)
在.NET 3.5+中动态生成代码的最简单方法是通过LambdaExpression
类的LINQ Expression Trees方法将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
。
答案 1 :(得分:2)
我不会那么快地解雇表达。您可以使用几种不同方式之一来表达目标。
如果您使用的是.NET 4+,则表达式树已经扩展为支持代码块。虽然您不能将此功能与lambda语法糖一起使用,但您可以使用Expression.Block
方法创建代码块。
使用对您要映射的每个字段都有参数的构造函数。生成的代码可以模仿return new T(ExtractA(t), ExtractB(t), ...)
。在这种情况下,您将从where T : new()
中删除Map<T>
约束,而是依赖于具有构造函数的对象模型类,该构造函数可以使用带有每个映射属性的参数的反射找到。
使用辅助方法执行一系列语句,就像它是单个语句一样。生成的代码可以模仿return ApplyProperties(t, new T(), new[] { applyA, applyB, ... })
,其中applyA
和applyB
是Action<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;
}
答案 2 :(得分: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"]));
答案 3 :(得分: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;
}
我在制作中使用了它,并且表现很好。