65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2013年5月26日

CPOL

4分钟阅读

viewsIcon

50287

使用 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 内部,它由三个阶段组成

  1. 删除实体
  2. 删除目标集合中不存在于我们的数据集合中的所有实体。这可以防止 EF 抛出上述错误。实体和 DTO 都有 Id,用于查找哪些项目被删除。这是基实体类有用的地方,因为它在内部定义了 Id。

  3. 映射更改的项目
  4. 如果找到了与数据集合中的项具有相同 Id 的实体,则将其用作映射的目标。

  5. 将新的(添加的)实体映射为新对象。
  6. 然后,这个通用类可以在 AutoMapper 配置文件中使用,如下所示

    CreateMap<ParentDTO,ParentEntity>()           
        .ForMember(o => o.DestinationCollection, m =>
                m.ResolveUsing(new EntityCollectionValueResolver<
                    ParentDTO, SourceDTO, DestEntity>
                    (s => s.SourceCollection))
                   )
    ;

还有一点:如果 SourceDTODestEntity 映射配置文件试图再次映射 ParenDTO -> ParentEntity,从 DestEntity 内部的 ParentEntity 属性,这个解决方案将导致 StackOverflowException。通常,子实体具有对父实体的引用。如果在映射过程中没有忽略它们,AutoMapper 将尝试进行映射:ParentDTO -> SourceCollection -> SourceDTO -> SourceEntity -> ParentDTO,这将导致循环映射。

此外,这个解析器不会涵盖目标集合是父项的派生项集合的情况。例如,当您有一个包含学生和教师的人员集合时,这将尝试仅对人员进行映射。所有派生类型数据将被忽略。

就是这样! Smile

© . All rights reserved.