使用AutoMapper映射Entity Framework实体集合

直接使用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>类型的表达式帮助确保在Resolve方法内能够获取正确的属性,而无需使用现有的对象源或创建一个新对象来传递到某个lambda中。GetPropertyValue方法是对象类型的扩展。它通过获取Expression>的MemberExpression,然后是source成员的MamberExpression.Member.Name。之后,可以使用源属性名称,通过反射获取其值:

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
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485