使用 AutoMapper 映射 EF 中的实体集合





5.00/5 (6投票s)
使用 AutoMapper 映射 EF 中的实体集合。
重要:
这是本文的旧版本。要查看更新版本,请点击:
在我上一篇文章中,我解释了在 EF 中添加基实体类的好处。今天,我将介绍如何通过使用这个基类,使用 AutoMapper 映射数据对象(即 DTO)的集合到现有实体集合。
这样做的缺点
dataColection.MapTo(entitiyCollection);
是 AutoMapper
会从实体集合中移除所有实体,因为映射到实体的数据项具有不同的哈希码和不同的引用,与原始实体不同。然后,当 AutoMapper 在原始实体集合中搜索与映射实体相同的项时,它找不到。这导致 AutoMapper
在移除原始实体后,添加另一个与原始实体具有相同 ID 的实体。以这种方式更改的实体集合无法保存到数据库,因为 EF 会报错说必须在提交时从数据库中显式删除已删除的实体。
为了解决这个问题,我们将使用自定义的 ValueResolver
。要创建一个,我们将创建一个从 AutoMapper
程序集中可用的 IValueResolver
派生的类。
public interface IValueResolver
{
ResolutionResult Resolve(ResolutionResult source);
}
还有一个可用的 ValueResolver<T1,T2>
public abstract class ValueResolver<TSource, TDestination> : IValueResolver
{
protected ValueResolver();
public ResolutionResult Resolve(ResolutionResult source);
protected abstract TDestination ResolveCore(TSource source);
}
但是,这个类只允许重写 ResolveCore
方法,这并不足够,因为它没有关于实体目标类型的信息。如果没有这些信息,我们将无法创建通用解析器类。因此,我们将使用接口而不是这个类。
我们的通用映射类必须采用两种类型的参数:数据对象 (DTO) 的类型和实体的类型。此外,自动映射器映射上下文的 ResolutionResult
对象没有关于在 ValueResolver
中映射哪个源成员的信息。这些信息也必须传递。最好将其作为表达式而不是 string
传递,以使其不易出错。为了使其成为可能,我们将添加第三个类型参数,它将是数据对象集合的父类型。
public class EntityCollectionValueResolver<TSourceParent, TSource, TDest> : IValueResolver
where TSource : DTOBase
where TDest : BaseEntity, new()
{
private Expression<Func<TSourceParent, ICollection>> sourceMember;
public EntityCollectionValueResolver(
Expression<Func<TSourceParent, ICollection>> sourceMember)
{
this.sourceMember = sourceMember;
}
public ResolutionResult Resolve(ResolutionResult source)
{
//get source collection
var sourceCollection = ((TSourceParent)source.Value).GetPropertyValue(sourceMember);
//if we are mapping to existing collection of entities...
if (source.Context.DestinationValue != null)
{
var destinationCollection = (ICollection<TDest>)
//get entities collection parent
source.Context.DestinationValue
//get entities collection by member name defined in mapping profile
.GetPropertyValue(source.Context.MemberName);
//delete entities that are not in source collection
var sourceIds = sourceCollection.Select(i => i.Id).ToList();
foreach (var item in destinationCollection)
{
if (!sourceIds.Contains(item.Id))
{
destinationCollection.Remove(item);
}
}
//map entities that are in source collection
foreach (var sourceItem in sourceCollection)
{
//if item is in destination collection...
var originalItem = destinationCollection.Where(
o => o.Id == sourceItem.Id).SingleOrDefault();
if (originalItem != null)
{
//...map to existing item
sourceItem.MapTo(originalItem);
}
else
{
//...or create new entity in collection
destinationCollection.Add(sourceItem.MapTo<TDest>());
}
}
return source.New(destinationCollection, source.Context.DestinationType);
}
//we are mapping to new collection of entities...
else
{
//...then just create new collection
var value = new HashSet<TDest>();
//...and map every item from source collection
foreach (var item in sourceCollection)
{
//map item
value.Add(item.MapTo<TDest>());
}
//create new result mapping context
source = source.New(value, source.Context.DestinationType);
}
return source;
}
}
类型为 Expression<Func<TSourceParent, ICollection>>
的表达式可以帮助我们确保在 Resolve
方法内部,我们将获得正确的属性,而无需使用现有的对象源或创建一个新的对象源来传递到某些 lambda 中。 GetPropertyValue
方法是对象类型的扩展。它通过从我们的 Expression<Func<TSourceParent, ICollection>>
中获取 MamberExpression
,然后获取源成员的属性 MamberExpression.Member.Name
来工作。之后,通过源属性名称,我们可以使用反射获取其值。
public static TRet GetPropertyValue<TObj, TRet>(this TObj obj,
Expression<Func<TObj, TRet>> 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<ParentDTO,ParentEntity>()
.ForMember(o => o.DestinationCollection, m =>
m.ResolveUsing(new EntityCollectionValueResolver<
ParentDTO, SourceDTO, DestEntity>
(s => s.SourceCollection))
)
;
还有一点:如果 SourceDTO
到 DestEntity
映射配置文件试图再次映射 ParenDTO
-> ParentEntity
,从 DestEntity
内部的 ParentEntity
属性,这个解决方案将导致 StackOverflowException
。通常,子实体具有对父实体的引用。如果在映射过程中没有忽略它们,AutoMapper
将尝试进行映射:ParentDTO
-> SourceCollection
-> SourceDTO
-> SourceEntity
-> ParentDTO
,这将导致循环映射。
此外,这个解析器不会涵盖目标集合是父项的派生项集合的情况。例如,当您有一个包含学生和教师的人员集合时,这将尝试仅对人员进行映射。所有派生类型数据将被忽略。
就是这样!