直接使用AutoMapper映射集合时会遇到一个问题:
dataCollection.MapTo(entityCollection);
AutoMapper会移除实体集合中的所有实体,因为映射到实体的数据项具有不同的哈希码和引用,与原始实体不同。当AutoMapper在原始实体集合中查找映射实体的相同项时,它找不到。这导致AutoMapper添加了与原始实体具有相同ID的另一个实体。以这种方式改变的实体集合无法保存到数据库,因为EF会抱怨被移除的实体需要在提交时显式从数据库中移除。
为了解决这个问题,将使用自定义的ValueResolver。要创建一个,将创建一个类,该类派生自AutoMapper程序集中的IValueResolver。
public interface IValueResolver
{
ResolutionResult Resolve(ResolutionResult source);
}
还有一个ValueResolver
public abstract class ValueResolver : IValueResolver
{
protected ValueResolver();
public ResolutionResult Resolve(ResolutionResult source);
protected abstract TDestination ResolveCore(TSource source);
}
但是这个类只允许覆盖ResolveCore方法,这是不够的,因为它没有关于实体的目标类型的信息。没有这些信息,就无法创建一个通用的解析器类。因此,将使用接口而不是这个类。
通用映射类需要接受两种类型的参数:数据对象(DTO)的类型和实体的类型。此外,AutoMapper映射上下文中的ResolutionResult对象没有关于正在映射的源成员的信息。这个信息也必须传递。最好是将其作为表达式传递,而不是字符串,以减少错误的可能性。为了实现这一点,将添加第三个类型参数,它将是数据对象集合的父类型。
public class EntityCollectionValueResolver : IValueResolver
where TSource : DTOBase
where TDest : BaseEntity, new()
{
private Expression> sourceMember;
public EntityCollectionValueResolver(
Expression> sourceMember)
{
this.sourceMember = sourceMember;
}
public ResolutionResult Resolve(ResolutionResult source)
{
// 获取源集合
var sourceCollection = ((TSourceParent)source.Value).GetPropertyValue(sourceMember);
// 如果正在映射到现有的实体集合...
if (source.Context.DestinationValue != null)
{
var destinationCollection = (ICollection)
// 获取实体集合的父级
source.Context.DestinationValue
// 通过映射配置文件中定义的成员名称获取实体集合.GetPropertyValue(source.Context.MemberName);
// 删除不在源集合中的实体
var sourceIds = sourceCollection.Select(i => i.Id).ToList();
foreach (var item in destinationCollection)
{
if (!sourceIds.Contains(item.Id))
{
destinationCollection.Remove(item);
}
}
// 映射源集合中的实体
foreach (var sourceItem in sourceCollection)
{
// 如果项在目标集合中...
var originalItem = destinationCollection.Where(o => o.Id == sourceItem.Id).SingleOrDefault();
if (originalItem != null)
{
// ...映射到现有项
sourceItem.MapTo(originalItem);
}
else
{
// ...或者在集合中创建新实体
destinationCollection.Add(sourceItem.MapTo());
}
}
return source.New(destinationCollection, source.Context.DestinationType);
}
// 正在映射到新的实体集合...
else
{
// ...那么只需创建新集合
var value = new HashSet();
// ...并将源集合中的每个项映射
foreach (var item in sourceCollection)
{
// 映射项
value.Add(item.MapTo());
}
// 创建新的映射上下文
source = source.New(value, source.Context.DestinationType);
}
return source;
}
}
Expression
public static TRet GetPropertyValue(this TObj obj,
Expression> expression, bool silent = false)
{
var propertyPath = ExpressionOperator.GetPropertyPath(expression);
var objType = obj.GetType();
var propertyValue = objType.GetProperty(propertyPath).GetValue(obj, null);
return propertyValue;
}
public static MemberExpression GetMemberExpression(Expression expression)
{
if (expression is MemberExpression)
{
return (MemberExpression)expression;
}
else if (expression is LambdaExpression)
{
var lambdaExpression = expression as LambdaExpression;
if (lambdaExpression.Body is MemberExpression)
{
return (MemberExpression)lambdaExpression.Body;
}
else if (lambdaExpression.Body is UnaryExpression)
{
return ((MemberExpression)((UnaryExpression)lambdaExpression.Body).Operand);
}
}
return null;
}
Resolve方法被包含在一个if语句中:
if (source.Context.DestinationValue != null)
这将确保覆盖两种情况:将数据集合映射到现有的实体集合和映射到新的实体集合。第二种情况在else内部并不复杂,因为它是集合内所有项的简单映射。有趣的部分发生在if内部,它由三个阶段组成:
删除实体:所有不在数据集合中的目的地集合中的实体都被删除。这可以防止EF抛出上述错误。实体和DTO都有ID,用于查找哪些项被删除。这就是基实体类有用的地方,因为它在内部定义了ID。
映射更改的项:如果在数据集合中找到了具有相同ID的实体,则将其用作映射的目标。
映射新(添加的)实体,作为新对象。
然后这个通用类可以像这样在AutoMapper配置文件中使用:
CreateMap()
.ForMember(o => o.DestinationCollection, m =>
m.ResolveUsing(
new EntityCollectionValueResolver(
(s => s.SourceCollection))
)
);
另外一件事:如果SourceDTO到DestEntity的映射配置文件尝试再次映射ParentDTO -> ParentEntity,这个解决方案将导致StackOverflowException。通常子实体有对父实体的引用。如果在映射过程中不忽略它们,AutoMapper将尝试进行映射:
ParentDTO -> SourceCollection -> SourceDTO -> SourceEntity -> ParentDTO