rhk98
rhk98

Reputation: 117

Automapper mapping IList<> to Iesi.Collections.Generic.ISet<>

I am having some issues in the mapping mentioned in the title. Here are the details:

class MyDomain
{
   public Iesi.Collections.Generic.ISet<SomeType> MySomeTypes{ get; set; }
   ....
}


class MyDTO
{
  public IList<SomeTypeDTO> MySomeTypes{ get; set; }
  ...
}

The mapping:

Mapper.CreateMap<MyDomain, MyDTO>().ForMember(dto=>dto.MySomeTypes, opt.ResolveUsing<DomaintoDTOMySomeTypesResolver>());

Mapper.CreateMap<MyDTO, MyDomain>().ForMember(domain=>domain.MySomeTypes, opt.ResolveUsing<DTOtoDomainMySomeTypesResolver>());

The Resolvers:

class DomaintoDTOMySomeTypesResolver: ValueResolver<MyDomain, IList<SomeTypeDTO>> 
{
  protected override IList<SomeTypeDTO> ResolveCore(MyDomain source)
  {
      IList<SomeTypeDTO> abc = new List<DemandClassConfigurationDTO>();
      //Do custom mapping
      return abc;
  }
}

class DTOtoDomainMySomeTypesResolver: ValueResolver<MyDTO, Iesi.Collections.Generic.ISet<SomeType>> 
{
   protected override Iesi.Collections.Generic.ISet<SomeType> ResolveCore(SystemParameterDTO source)
   {
     Iesi.Collections.Generic.ISet<SomeType> abc = new HashedSet<SomeType>();
     //Do custom mapping
     return abc;
   }
}

Mapping from Domain to DTO works ok and as expected I get a MyDTO object with IList of "SomeTypeDTO" objects. However mapping of the DTO to Domain throws the following error:

 Exception of type 'AutoMapper.AutoMapperMappingException' was thrown.
  ----> AutoMapper.AutoMapperMappingException : Trying to map  Iesi.Collections.Generic.HashedSet`1[SomeType, MyAssembly...] to Iesi.Collections.Generic.ISet`1[SomeType, MyAssembly...]

 Exception of type 'AutoMapper.AutoMapperMappingException' was thrown.
  ----> System.InvalidCastException : Unable to cast object of type 'System.Collections.Generic.List`1[SomeType]' to type 'Iesi.Collections.Generic.ISet`1[SomeType]

What might I be doing wrong and what do the error messages imply? It almost seems that automapper is having some issues in mapping the ISet ( together with its concrete implementation HashedSet). My understanding is that in the above described scenario automapper should just use the ISet reference returned by "DTOtoDomainMySomeTypesResolver". I also don't see why I am getting the "cast from List to ISet error".

Upvotes: 4

Views: 2086

Answers (1)

MSkuta
MSkuta

Reputation: 1783

This is because AutoMapper currently doesn't support ISet<> collection properties. It works when the destination property of ISet<> is already instantiated (is not null), because the ISet<> actually inherits from ICollection<>, thus Automapper can understand that and will do the collection mapping properly.

It doesn't work when the destination property is null and is interface type. You get this error, because automapper actually found out it can be assigned from ICollection<> so it instantiates the property using generic List<>, which is default collection when automapper must create new collection property, but then when it tries to actually assign it, it will fail, because obviously List<> cannot be cast to ISet<>

There are three solution to this:

  1. Create a feature request to support ISet<> collections and hope they will add it
  2. Make sure the property is not null. Eg. instantiate it in constructor to empty HashSet<>. This might cause some troubles for ORM layers, but is doable
  3. The best solution that I went with is to create custom value resolver, which you already have and instantiate the property yourself if it is null. You need to implement the IValueResolver, because the provided base ValueResolver will not let you instantiate the property. Here is the code snippet that I used:


public class EntityCollectionMerge : IValueResolver
        where TDest : IEntityWithId
        where TSource : IDtoWithId
    {
        public ResolutionResult Resolve(ResolutionResult source)
        {
            //if source collection is not enumerable return
            var sourceCollection = source.Value as IEnumerable;
            if (sourceCollection == null) return source.New(null, typeof(IEnumerable));

            //if the destination collection is ISet
            if (typeof(ISet).IsAssignableFrom(source.Context.DestinationType))
            {
                //get the destination ISet
                var destSet = source.Context.PropertyMap.GetDestinationValue(source.Context.DestinationValue) as ISet;
                //if destination set is null, instantiate it
                if (destSet == null)
                {
                    destSet = new HashSet();
                    source.Context.PropertyMap.DestinationProperty.SetValue(source.Context.DestinationValue, destSet);
                }
                Merge(sourceCollection, destSet);
                return source.New(destSet);
            }

            if (typeof(ICollection).IsAssignableFrom(source.Context.DestinationType))
            {
                //get the destination collection
                var destCollection = source.Context.PropertyMap.GetDestinationValue(source.Context.DestinationValue) as ICollection;
                //if destination collection is null, instantiate it
                if (destCollection == null)
                {
                    destCollection = new List();
                    source.Context.PropertyMap.DestinationProperty.SetValue(source.Context.DestinationValue, destCollection);
                }
                Merge(sourceCollection, destCollection);
                return source.New(destCollection);
            }

            throw new ArgumentException("Only ISet and ICollection are supported at the moment.");
        }




        public static void Merge(IEnumerable source, ICollection destination)
        {
            if (source == null) return;

            var destinationIds = destination.Select(x => x.Id).ToHashSet();
            var sourceDtos = source.ToDictionary(x => x.Id);

            //add new or update
            foreach (var sourceDto in sourceDtos)
            {
                //if the source doesnt exist in destionation add it
                if (sourceDto.Key (sourceDto.Value));
                    continue;
                }

                //update exisiting one
                Mapper.Map(sourceDto.Value, destination.First(x => x.Id == sourceDto.Key));
            }

            //delete entity in destination which were removed from source dto
            foreach (var entityToDelete in destination.Where(entity => !sourceDtos.ContainsKey(entity.Id)).ToList())
            {
                destination.Remove(entityToDelete);
            }
        }
    }


Then on your mapping use opt => opt.ResolveUsing(new EntitCollectionMerge<Entity,Dto>()).FromMember(x => x.ISetMember) or if you have lots of collection like this you can add them automatically to all of them via typeMaps.

Upvotes: 2

Related Questions