Click here to Skip to main content
12,446,146 members (82,307 online)
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

24.1K views
7 bookmarked
Posted

Mapping Collection of Entities in EF with AutoMapper

, 26 May 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Mapping collection of entities in EF with AutoMapper.

Important:

This is an old version of this article. To see updated version please click on:

In my last post, I explained why it is useful to add base entity class in EF. Today, I will write how with the use of this base class an AutoMapped map collection of data objects (i.e. DTOs to existing collection of entities).

Problem with doing:

dataColection.MapTo(entitiyCollection);

is that AutoMapper removes all entities from entity collection because data item mapped to entity has different hash code and different reference than the original entity. Then when AutoMapper searches for the same item in the original entity collection as mapped entity, it cannot find one. That causes AutoMapper to add another entity with the same ID as the original, after removing the original entity. Entity collection changed in that way cannot be saved to the database, because EF complains that removed entities have to be removed explicitly from the database on commit.

To fix that problem, we will use a custom ValueResolver. To create one, we will create a class which will be derived from the IValueResolver available in the AutoMapper assembly.

public interface IValueResolver
{
    ResolutionResult Resolve(ResolutionResult source);
}

There is also available a ValueResolver<T1,T2>:

public abstract class ValueResolver<TSource, TDestination> : IValueResolver
{
    protected ValueResolver();

    public ResolutionResult Resolve(ResolutionResult source);
    protected abstract TDestination ResolveCore(TSource source);
}

But this class makes it available to override only the ResolveCore method, which will not be sufficient since it does not have information about the destination type of the entity. Without this information, we won't be able to create a generic resolver class. So instead of this class, we will use the interface.

Our generic mapping class has to take two types of parameters: type of data object (DTO) and type of entity. Also, the ResolutionResult object of the auto mapper mapping context does not have information of which source member is being mapped inside the ValueResolver. This information has to be passed too. It is best to pass it as an expression instead of a string, to make it less error prone. To make it possible, we will add a third type parameter which will be the parent type of the data object collection.

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 of type Expression<Func<TSourceParent, ICollection>> helps us to make sure that inside Resolve method we will get the correct property without necessity of using existing object source or creating a new one to pass inside some lambda. The GetPropertyValue method is an extension of object type. It works by taking MamberExpression from our Expression<Func<TSourceParent, ICollection>>, and then property MamberExpression.Member.Name of source member. After that with source property name, we can take its value with reflection:

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;
}

The Resolve method is enclosed in an if statement:

if (source.Context.DestinationValue != null)

This will ensure that we cover two cases when we map a data collection to an existing collection of entities and to a new collection of entities. The second case is inside the else and is not complicated since it is a simple mapping of all items inside a collection. The interesting part is happening inside the if and it is composed of three phases:

  1. Deleting of entities
  2. All entities from destination collection, that are not present inside our data collection, are being deleted. That prevents EF from throwing an error mentioned above. Entities and DTOs have both Ids, which are used to find which of the items was deleted. This is where base entity class is useful since it has Id defined inside.

  3. Mapping changed items
  4. If entity with the same Id as item in data collection has been found, it is being used as destination of mapping.

  5. Mapping of new (added) entities, as new objects.
  6. This generic class then can be used as this inside AutoMapper profile:

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

One more thing: this solution will cause StackOverflowException if SourceDTO to DestEntity mapping profile will try to map again ParenDTO -> ParentEntity, from ParentEntity property inside DestEntity. Usually child entities has reference to parent entities. If they are not ignored during mapping, AutoMapper will try do mapping: ParentDTO -> SourceCollection -> SourceDTO -> SourceEntity -> ParentDTO which will cause circular mapping.

Also this resolver will not cover the case when Destination Collection is a collection of derived items from parent item. For example, when you have a collection of people with students and teachers inside it, this will try to do mapping only for people. All derived types data will be ignored.

That is all! Smile

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

n.podbielski
Software Developer
Poland Poland
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionExpressionOperator missing Pin
Michal Turecki12-Feb-14 22:23
memberMichal Turecki12-Feb-14 22:23 
AnswerRe: ExpressionOperator missing Pin
Michal Turecki12-Feb-14 22:30
memberMichal Turecki12-Feb-14 22:30 
GeneralRe: ExpressionOperator missing Pin
n.podbielski24-Feb-14 20:24
membern.podbielski24-Feb-14 20:24 
AnswerRe: ExpressionOperator missing Pin
n.podbielski13-Feb-14 13:18
membern.podbielski13-Feb-14 13:18 
AnswerRe: ExpressionOperator missing Pin
n.podbielski20-Feb-14 0:14
membern.podbielski20-Feb-14 0:14 
GeneralRe: ExpressionOperator missing Pin
thatraja20-Feb-14 4:01
protectorthatraja20-Feb-14 4:01 
GeneralRe: ExpressionOperator missing Pin
n.podbielski20-Feb-14 23:16
membern.podbielski20-Feb-14 23:16 
GeneralRe: ExpressionOperator missing Pin
thatraja21-Feb-14 2:14
protectorthatraja21-Feb-14 2:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.160811.3 | Last Updated 26 May 2013
Article Copyright 2013 by n.podbielski
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid