sheng.c
E-mail: cao.silhouette(at)msn.com
QQ: 279060597
https://github.com/iccb1013
C#
WPF
Github 开源: Sheng.Mapper 与 AutoMapper 互补,大幅提高开发效率
开源(GitHub) C# 2017/8/16 17:52:04

Github 地址https://github.com/iccb1013/Sheng.Mapper


对象属性值映射/拷贝工具。不需要创建映射规则,不要求对象类型一致,适用于简单直接的拷贝操作,可以全属性拷贝,指定属性拷贝,排除指定的属性。拷贝包含 10 个属性的对象 10 万次,耗时 4.x 秒(普通开发机)。


+ 拷贝行为只针对 sourceObject 和 targetObject 所共有的属性
+ 在 sourceObject 和 targetObject 中的待拷贝的属性值的类型处理:如果是值类型,直接拷贝,如果是引用类型,sourceObject 中的属性的类型 必须 和 targetObject 中的属性的类型一致,或是它的派生类
+ 如果要支持类型不一致的属性自动进行类型转换,你可以在 PropertyMappingDescription 这个类中实现转换器功能
+ 拷贝行为 不会 改变 targetObject 中不需要被拷贝的属性的值
+ 你可以组合使用几个方法来从多个对象中拷贝指定的属性值到一个 targetObject

和 AutoMapper 互补,与之相比最大优势是短,平,快。不需要创建复杂的映射规则,并支持属性排除操作。


具体实现:

这里在具体实现上,其实并不复杂,只需对反射操作稍有了解即可,

我们通过 sourceObject 和 targetObject ,获取它们的“类型(Type)”,然后使用 Type.GetProperties() 方法,获取这个对象类型所包含的属性(Property)。

PropertyInfo[] propertyList = Type.GetProperties();
foreach (PropertyInfo property in propertyList)
{
  PropertyMappingDescription propertyMappingDescription = new PropertyMappingDescription(property);
  _propertyList.Add(propertyMappingDescription);
    _propertyNames.Add(property.Name, propertyMappingDescription);
}

这里有另外一个细节需要留意的是,我们要把同样类型(Type)的相关信息,缓存起来,这样下次再拷贝相同类型的对象时,就无需再去反射它的 Properties。

我们通过 TypeMappingDescription 对对象的类型信息进行缓存和包装,提供我们所需要的基本操作:

public bool ContainsProperty(string name)
{
    if (String.IsNullOrEmpty(name))
        throw new ArgumentNullException("TypeMappingDescription.ContainsProperty 必须指定属性名。");
    return _propertyNames.ContainsKey(name);
}
public object GetValue(object obj, string propertyName)
{
    if (obj == null)
        throw new ArgumentNullException("指定的对象为空。");
    if (obj.GetType() != this.Type)
        throw new ArgumentException("指定的对象类型与缓存的对象类型不一致。");
    if (_propertyNames.ContainsKey(propertyName) == false)
        throw new ArgumentOutOfRangeException("指定的属性名不存在。");
    PropertyMappingDescription propertyMappingDescription = (PropertyMappingDescription)_propertyNames[propertyName];
    if (propertyMappingDescription.CanRead == false)
        throw new InvalidOperationException("属性 " + propertyName + "不可读。");
    return propertyMappingDescription.GetValue(obj);
}
public void SetValue(object obj, string propertyName, object value)
{
    if (obj == null)
        throw new ArgumentNullException("指定的对象为空。");
    if (obj.GetType() != this.Type)
        throw new ArgumentException("指定的对象类型与缓存的对象类型不一致。");
    if (_propertyNames.ContainsKey(propertyName) == false)
        throw new ArgumentOutOfRangeException("指定的属性名不存在。");
    PropertyMappingDescription propertyMappingDescription = (PropertyMappingDescription)_propertyNames[propertyName];
    if (propertyMappingDescription.CanWrite == false)
        throw new InvalidOperationException("属性 " + propertyName + "只读。");
    Type propertyType = propertyMappingDescription.PropertyInfo.PropertyType;
    if (propertyType.IsValueType == false && value != null)
    {
        Type valueType = value.GetType();
        if (propertyType != valueType && valueType.IsSubclassOf(propertyType) == false)
        {
            throw new ArgumentException("目标对象的 " + propertyName + "与 value 的类型既不一致,也不是目标类型的派生类。");
        }
    }
    propertyMappingDescription.SetValue(obj, value);
}


同时我们使用 PropertyMappingDescription 对 PropertyInfo 进行封装。对 PropertyInfo 进行封装,是为了方便我们后续针对属性添加属性值的转换器,以便实现稍复杂一些的属性拷贝操作。


最后我们来测试一下拷贝操作:

A a = new A()
{
    Name = " 张三",
    Age = 10,
    Class = "一班",
    CObject = new SubC()
    {
        Message = " Hello & quot;
    },
    P1 = "1",
    P2 = "2",
    P3 = "3",
    P4 = "4",
    P5 = "5",
    P6 = "6"
};
B b = new B();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 100000; i++)
{
    //全部属性拷贝
    ShengMapper.SetValues(a, b);
    //拷贝指定的属性
    // ShengMapper.SetValuesWithProperties(a, b, new string[] { "Name", "Age", "P1" });
    //排除指定的属性
    //ShengMapper.SetValuesWithoutProperties(a, b, new string[] { "Name", "Age", "P1" });
                
}
stopwatch.Stop();
Console.WriteLine("对包含 10 个属性的对象的属性值拷贝 10 万次,耗时:" + stopwatch.Elapsed.ToString());
Console.ReadLine();


我模拟了一几个类,他们有不同类型的属性,还包括引用类型的属性我派生类。

对于包含 10 个属性的类的 10 万次属性值拷贝,在开发机上大约用了 4.x 秒。


下面直接用几个例子来具体的讲一下它的使用场景,和为什么要使用它,以及和 AutoMapper 的互补使用。

AutoMapper 是一款十分强大的对象映射工具,我在项目中大量的使用了 AutoMapper 这一工具,如处理 DTO 与 实体对象间的映射关系,使用类似如下代码:

78019-20170809122113402-1432617548.png


但是在使用 AutoMapper 的过程中,有一些小细节会比较麻烦,它不能创建相同对象类型间的映射赋值关系,也不能根据场景为相同的对象映射关系创建不同的映射逻辑,所以我们目前的使用集中的 Dto 与 实体对象间的映射。

我们现在的项目使用了 Entity Framework ,在更新数据时,一般的逻辑是前端传过来一个 DTO 对象,转换成实体对象,根据 Id 去 DBContext 中拿到数据库中的实体对象,再用传入的实体对象给数据库中的对象赋值,SaveChanges。


类似如下代码,用传入的 storehouseBase 给 dbStorehouseBase 赋值:

78019-20170809123226339-2135704050.png


这里如果使用 AutoMapper   就涉及到两个问题:

1.AutoMapper  是不能为相同的对象类型创建映射规则的,如:

78019-20170809122740027-1398287912.png


2.如果不使用规则,即使可以直接 Map,实体对象中又存在大量的导航属性,是不能直接覆盖过去的,这就比较麻烦了,我们的项目中开发人员只好手工的用传入的实体对象,一个属性一个属性为数据库中拿到的实体对象赋值,如果要更新的属性比较多,就非常麻烦,此外在修改时给实体对象新增加了属性,是否还能保证记得回到这里来给添加新的赋值代码。


所以在这种场景下, 可以使用 ShengMapper 来解决:

 78019-20170809123332964-2110239767.png

 

ShengMapper 的 SetValuesSkipVirtual 方法,见文知义,它自动映射两个对象,进行赋值操作,并自动的跳过 Virtual  属性,因为 Entity Framework 的导航属性都是 Virtual   的(DB First)。

 

此外,还有一些更新赋值场景,存在一些特定的业务逻辑,比如说我更新用户时,不更新 Password 字段,不更新最后登录时间字段,更新订单时,不更新创建时间字段等等,这种同一种对象类型的映射赋值,不但要跳过导航属性,还要跳过指定的字段,也可以使用 ShengMapper  很好的解决,使用类似如下代码:

78019-20170809123915933-564867540.png


它的方法原型是:

78019-20170809123943683-1095895454.png


SetValuesWithoutProperties 方法 把 product 中的属性,更新到 dbProduct 中,同时跳过 Supplier 属性和所有的 Virtual 属性。


那么既然能跳过指定的属性,自然我们也能只更新指定的属性,如:


SetValuesWithProperties 方法把 productEntryBatch 中的属性值更新到 dbProductEntryBatch 中,但是只更新 Name 这个属性。


ShengMapper 还提供了一些其它方法重载:

78019-20170809125219120-602221690.png


使用 ShengMapper 是不需要事先创建规则的,所有对对象的映射赋值操作,都是以更灵活的方式来完成的,比如我这一次 User 对 User 要排除 Password ,另一个场景不需要排除,也是可以的,而 AutoMapper 的规则必须是全局唯一的,一旦创建了 UserDTO 对 User 的规则,在所有的时候,都会遵循事先定义的规则去执行。

所以在实际应用中,可以通过与 AutoMapper 互补的方式,解决这方面的问题。

您的称呼:
留言: