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)
{
var sourceCollection = ((TSourceParent)source.Value).GetPropertyValue(sourceMember);
if (source.Context.DestinationValue != null)
{
var destinationCollection = (ICollection<TDest>)
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<TDest>());
}
}
return source.New(destinationCollection, source.Context.DestinationType);
}
else
{
var value = new HashSet<TDest>();
foreach (var item in sourceCollection)
{
value.Add(item.MapTo<TDest>());
}
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:
- Deleting of entities
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.
- Mapping changed items
If entity with the same Id as item in data collection has been found, it is being used as destination of mapping.
- Mapping of new (added) entities, as new objects.
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!