Github 开源: Sheng.Mapper 与 AutoMapper 互补,大幅提高数据库开发效率
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";
},
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 与 实体对象间的映射关系,使用类似如下代码:
但是在使用 AutoMapper 的过程中,有一些小细节会比较麻烦,它不能创建相同对象类型间的映射赋值关系,也不能根据场景为相同的对象映射关系创建不同的映射逻辑,所以我们目前的使用集中的 Dto 与 实体对象间的映射。
我们现在的项目使用了 Entity Framework ,在更新数据时,一般的逻辑是前端传过来一个 DTO 对象,转换成实体对象,根据 Id 去 DBContext 中拿到数据库中的实体对象,再用传入的实体对象给数据库中的对象赋值,SaveChanges。
类似如下代码,用传入的 storehouseBase 给 dbStorehouseBase 赋值:
这里如果使用 AutoMapper 就涉及到两个问题:
1.AutoMapper 是不能为相同的对象类型创建映射规则的,如:
2.如果不使用规则,即使可以直接 Map,实体对象中又存在大量的导航属性,是不能直接覆盖过去的,这就比较麻烦了,我们的项目中开发人员只好手工的用传入的实体对象,一个属性一个属性为数据库中拿到的实体对象赋值,如果要更新的属性比较多,就非常麻烦,此外在修改时给实体对象新增加了属性,是否还能保证记得回到这里来给添加新的赋值代码。
所以在这种场景下, 可以使用 ShengMapper 来解决:
ShengMapper 的 SetValuesSkipVirtual 方法,见文知义,它自动映射两个对象,进行赋值操作,并自动的跳过 Virtual 属性,因为 Entity Framework 的导航属性都是 Virtual 的(DB First)。
此外,还有一些更新赋值场景,存在一些特定的业务逻辑,比如说我更新用户时,不更新 Password 字段,不更新最后登录时间字段,更新订单时,不更新创建时间字段等等,这种同一种对象类型的映射赋值,不但要跳过导航属性,还要跳过指定的字段,也可以使用 ShengMapper 很好的解决,使用类似如下代码:
它的方法原型是:
SetValuesWithoutProperties 方法 把 product 中的属性,更新到 dbProduct 中,同时跳过 Supplier 属性和所有的 Virtual 属性。
那么既然能跳过指定的属性,自然我们也能只更新指定的属性,如:
SetValuesWithProperties 方法把 productEntryBatch 中的属性值更新到 dbProductEntryBatch 中,但是只更新 Name 这个属性。
ShengMapper 还提供了一些其它方法重载:
使用 ShengMapper 是不需要事先创建规则的,所有对对象的映射赋值操作,都是以更灵活的方式来完成的,比如我这一次 User 对 User 要排除 Password ,另一个场景不需要排除,也是可以的,而 AutoMapper 的规则必须是全局唯一的,一旦创建了 UserDTO 对 User 的规则,在所有的时候,都会遵循事先定义的规则去执行。
所以在实际应用中,可以通过与 AutoMapper 互补的方式,解决这方面的问题。