使用LINQ表达式将值分配给对象的属性

10

我正在使用一个旧的数据模型,而且必须按照手头上已有的进行工作。

当我执行数据库查询时,该模型返回的数据是

List<Dictionary<string, object>>

对于每个字典,键是列名,值是列值。可以想象,使用它需要大量的foreach循环和类型转换,非常麻烦。

我希望定义一些POCO视图模型,然后使用LINQ /反射和“分配绑定映射”将丑陋的返回值转换为我的干净的POCO。因此,我可以定义“映射”,其中包含列名称和针对POCO属性的lambda表达式,类似于以下示例...

var Map; // type???
Map.Add("Id", p => p.Id);
Map.Add("Code", p => p.Code);
Map.Add("Description", p => p.Description);
Map.Add("Active", p => p.Active);

然后按照以下方式进行转换...

List<Dictionary<string, object>> Results = MyModel.Query(...);
List<ProductViewModel> POCOs = new List<ProductViewModel>();

foreach (var Result in Results) // Foreach row
{
  ProductViewModel POCO = new ProductViewModel();

  foreach (var i in Result) // Foreach column in this row
  {
    // This is where I need help.
    // i.Key is the string name of my column.
    // I can get the lambda for this property from my map using this column name.
    // For example, need to assign to POCO.Id using the lambda expression p => p.Id
    // Or, assign to POCO.Code using the lambda expression p => p.Code
  }

  POCOs.Add(POCO);
}

return POCOs;

使用某种反射方式可以做到这点吗?如果可以,怎么做?

3个回答

18

这里提供了一种使用表达式树的方法。首先,定义地图的API:

public class PropertyMap<T> where T : new()
{
    public void Add(string sourceName, Expression<Func<T, object>> getProperty);

    public T CreateObject(IDictionary<string, object> values);
}

你会这样使用它:
var map = new PropertyMap<ProductViewModel>();

map.Add("Id", p => p.Id);
map.Add("Code", p => p.Code);
map.Add("Description", p => p.Description);
map.Add("Active", p => p.Active);

var productViewModel = map.CreateObject(values);

要实现它,首先需要声明一个字典,将数据源中的名称与属性关联:

private readonly IDictionary<string, PropertyInfo> _properties = new Dictionary<string, PropertyInfo>();

接下来,你需要根据字典实现Add方法(所有的错误处理都留给读者自己练习):

public void Add(string sourceName, Expression<Func<T, object>> getProperty)
{
    _properties[sourceName] = (PropertyInfo) ((MemberExpression) getProperty.Body).Member;
}

然后,您将使用表达式树动态编译一个方法来执行赋值操作(听起来比实际情况要可怕得多)。最简单的方式是查看我们正在构建的示例。我们需要的是一些代码,它可以完成以下操作:

new ProductViewModel
{
    Id = ...,
    Code = ...,
    Description = ...,
    Active = ...
}

但是,由于动态映射,我们无法在编译时知道这一点。因此,我们将构建一个函数,该函数是完全相同的代码,但在运行时编译。表达式树只是表示您可以在编译时编写的相同代码的运行时数据。

首先,我们需要获取属性的一组绑定(赋值):

private IEnumerable<MemberBinding> GetPropertyBindings(IDictionary<string, object> values)
{
    return
        from sourceName in _properties.Keys
        select Expression.Bind(_properties[sourceName], Expression.Constant(values[sourceName]));
}

我们在这里说的是,对于映射属性中的每个属性,查找其值并将其作为常量(例如对于Id,可能是值7),然后将相应的属性绑定到它。这给我们带来了表达式Id = 7。我们对所有属性重复此过程,从而得到所有的赋值语句。
一旦我们有了这些绑定,我们就可以创建完整的成员初始化,包括构造函数调用:
private MemberInitExpression GetMemberInit(IDictionary<string, object> values)
{
    return Expression.MemberInit(Expression.New(typeof(T)), GetPropertyBindings(values));
}

因为我们在类声明中指定了 where T : new(),所以我们保证有一个无参构造函数可以在这里调用。我们传递之前创建的属性绑定,得到了一个表示我们想要构建的初始化表达式的数据结构。
那么现在我们该怎么做呢?我们拥有了这个数据结构,但是如何调用这段代码呢?为了做到这点,我们必须将该表达式包装在一个可调用的函数中,因为你实际上只能调用方法。这意味着我们正在构建以下代码:
() => new ProductViewModel
{
    Id = ...,
    Code = ...,
    Description = ...,
    Active = ...
}

这是一个无参函数,当调用时将返回初始化的对象。这也被称为lambda表达式。我们可以按以下方式获取此数据结构:

private Func<T> GetInitializationFunction(IDictionary<string, object> values)
{
    var initializationLambda = Expression.Lambda<Func<T>>(GetMemberInit(values));

    return initializationLambda.Compile();
}

我们创建了一个Lambda表达式,其主体是成员初始化,这正是我们上面编写的代码。我们指定委托类型Func<T>,因为它不带参数并返回映射类型的对象。
然后,我们编译它。这个调用生成一个具有签名Func<T>的方法,我们可以调用它,并且它的主体是我们创建的数据结构作为代码。这是一种不直接使用反射进行反射的巧妙方式。
最后,我们通过创建函数并调用它来实现我们之前定义的CreateObject方法,从而给我们一个T(在这里是ProductViewModel)的实例:
public T CreateObject(IDictionary<string, object> values)
{
    var initializationFunction = GetInitializationFunction(values);

    return initializationFunction();
}

哇,非常感谢!与直接使用反射相比,这种方法的性能如何?我猜一旦表达式被编译,它就像本地代码一样快,对吗? - Stevoman
3
正确的 - 一旦你有了那个 Func<T>,就好像你传递了一个自己编写的方法的引用一样。此外,在这里还有丰富的缓存机会;例如,您可能只在第一次添加映射之后生成方法。然后,如果您将 PropertyMap<T> 实例缓存到应用程序级别,开销非常小。 - Bryan Watts

2
你可以通过以下方式从类似 p => p.Id 的 linq 表达式中提取属性名称:
public static string GetPropertyName<T>(Expression<Func<T>> expression)
{
    MemberExpression body = (MemberExpression)expression.Body;
    return body.Member.Name;
}

然后使用普通的反射将值分配给对象实例。例如,创建一个方法

private void Assign(object objInstance, Expression<Func<T>> propertyExpression, object value)
{
    string propertyNameToAssign = GetPropertyName(propertyExpression);

    //TODO use reflection to assign "value" to the property "propertyNameToAssign" of "objInstance"
}

(没有编译代码;关于反射部分,网络上有很多文章。希望这能帮到你)


C# 4.0似乎添加了一些称为表达式树的东西,它们对此有用吗?如果有用,与您展示的反射相比,它们的性能如何? - Stevoman
我实际上并没有太多涉及表达式树的工作经验,但据我所知,它们只是一种在某些数据上施加结构的方式,基本上是通过在这些所谓的表达式树中编码信息。实际上,GetPropertyName 已经在某种程度上使用了这样的表达式。然而,最终你仍然需要使用反射来提取实际值(据我所知)。我目前正在研究 AutoMapper 是否适用于此处,但我有些怀疑。 - Juri

0

这似乎是 dynamic 的完美匹配。您可以创建一个动态类,其属性基于字典中的键。请查看 DynamicObject 类。


2
这不是一个流行词,而是一个关键词。 - Erix

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