In my last post I explained why is useful to add base entity class in EF. Today I will write how with 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 then original entity. Then when AutoMapper search for same item in original entity collection as mapped entity, it can not find one. That is causing AutoMapper to ads another one entity with the same Id as original, after removing original entity. Entity collection changed in that way cannot be saved to database, because EF complaints that removed entities has to be removed explicitly from database on commit.
To fix that problem we will use custom ValueResolver
. To create one we will create class which will derive from IValueResolver
available in AutoMapper assembly.
public interface IValueResolver { ResolutionResult Resolve(ResolutionResult source); }
There is also available 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 make available to override only ResolveCore
method, which will be not sufficient since it does not have information about destination type of entity. Without this information we wont be able to create generic resolver class. So instead this class we will use interface.
Our generic mapping class has to take two type parameters type of data object (DTO) and type of entity. Also ResolutionResult
object of auto mapper mapping context does not have information of which source member is being mapped inside ValueResolver
. This information has to be passed to. It is best to passed it as expression instead of a string, to make it less error prone. To make it possible we will add third type parameter which will be parent type of 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.ToList()) { 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>>
help as to make sure that inside Resolve method we will get correct property without necessity of using existing object source or creating new one to pass in inside some lambda.
GetPropertyValue
method is extension of object type. It works by taking MemberExpression
from our Expression<Func<TSourceParent, ICollection>>
, and then property MemberExpression.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; }
Whole Resolve method is enclosed in if
statement:
if (source.Context.DestinationValue != null)
this will ensure that we cover 2 case when we map data collection to existing collection of entities and to new collection of entities. Second case is inside else
and is not complicated since it is simple mapping of all items inside collection.
Interesting part is happening inside if
and it is composed from three phases:
1. 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 items was deleted. This is where base entity class is useful since it has Id defined inside.
2. 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
3. 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 ParentDTO -> 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 case when Destination Collection is collection of derived items from parent item. For example when you have 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.
Unfortunately this will be not enough to map collection. Its because even items are removed collection, they are not set for deletion inside DbContext
class. This will cause critical error in application during SaveChanges
method in DbContext
. To correct that issue we have to mark them for deletion from context class.
To do that there are 3 options:
1. Use context class inside EntityCollectionValueResolver
class and mark deleted items for deletion. This is less elegant, but much more quick solution.
2. Use custom collection class which will mark deleted items for deletion using context class
3. Use custom collection class with items state tracking. This collection could subscribe an OnSaveChanges
event of DBContext
class in which event handler delete from context items deleted before from collection.
First and second options (maybe third too, it depends from implementation) will suffer from necessity to synchronize DbContext
which will be used to save changes inside parent entity. Entity which was mapped from DTO. Also first solution is less elegant because is mixing AutoMapper
and DbContext
. Those two should live separately.
In this article I will show second option since third, which is better I think will involve changing of entities classes and repositories. It’s to much for one article.
First, we have to acquire instance of context class. In my application I have IoC container which have single-for-thread instance of this class. This makes sure of synchronization of Context which loaded parent entity, deletes child entities and will save changes to parent entity.
At the beginning of the method Resolve
we will add code that will return current instance of Context class (example with using Microsoft Patterns & Practices IServiceLocator
implementation):
var context = ServiceLocator.GetInstance<DbContext>();
With this instance we can delete items from context:
if (!sourceIds.Contains(item.Id)) { destinationCollection.Remove(item); ((IObjectContextAdapter)context).ObjectContext.DeleteObject(item); }
After that ObjectContextManager
private property _entriesWithConceptualNulls
, will have 0 items, which is good because any item in this collection will cause EF to throw critical error.
With breakpoint set after line with DeleteObject
method call, you can see this collection with expression:
(context.Database._internalContext).ObjectContext.ObjectStateManager._entriesWithConceptualNulls
as like in the image:
This is whole body of Resolve method:
public ResolutionResult Resolve(ResolutionResult source) { var context = ServiceLocator.GetInstance<DbContext>(); //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) //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.ToList()) { if (!sourceIds.Contains(item.Id)) { destinationCollection.Remove(item); ((IObjectContextAdapter)context).ObjectContext.DeleteObject(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()); } } 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(); //...and map every item from source collection foreach (var item in sourceCollection) { //map item value.Add(item.MapTo()); } //create new result mapping context source = source.New(value, source.Context.DestinationType); } return source; } }
From now on after mapping from DTO to entity with automapper and saving mapped entity to database should work just fine.
That is all! 🙂
Hello, could you please provide working sample? There are some dependencies missing and code is not compilable. Would be really appreciated!
Regards, Jozef.
hello
this code is not work in EF core 2.0. can you help me?