AutoMapper & EntityCollection<T> properties

Posts   
 
    
worldspawn avatar
worldspawn
User
Posts: 321
Joined: 26-Aug-2006
# Posted on: 02-Mar-2010 08:23:07   

AutoMapper does not like properties of type EntityCollection<T>. I've just spent a few hours rummaging around through AutoMapper source code and found that an entitycollection when cast to IListSource returns an EntityView from GetList().

This causes the first issue which is that ListSourceMapper calls Clear on that returned IList which throws a NotSupportedException.

If you work through that (the how not being important just now) the next problem you have is that EntityCollection properties of your entities are all readonly and AutoMapper tries to SET them to a new entitycollection.

AutoMapper is not a joy to extend. Rather than making the ConfigurationProvider settable (if you can't set why the smeg even have one???). Instead there are a bunch of wacky static delegates that return an array of all the type mappers and the mappers used by automapper. This is where you can start fiddling.

Annoying caveat: All the AutoMappers type mappers are "private" classes. So you can't "modify" any of them. I copied them all out of the source and re-implemented them (so to speak) with my changes. Fortunately there are only five.

ListSourceMapper.GetEnumerableFor was changed to:


protected override IList GetEnumerableFor(object destination)
        {
            if (destination is IList)//newline
                return (IList)destination;//newline
            var listSource = (IListSource)destination;
            return listSource.GetList();
        }

Secondly the PropertyMapMappingStrategy.AssignValue was modified like so:


protected virtual void AssignValue(PropertyMap propertyMap, object mappedObject, object propertyValueToAssign)
        {
            if (!propertyMap.UseDestinationValue && propertyMap.CanBeSet)
                propertyMap.DestinationProperty.SetValue(mappedObject, propertyValueToAssign);
            else//newline
            {//newline
                if (!propertyMap.CanBeSet && propertyValueToAssign is IEntityCollection2)//newline
                {//newline
                    var ec = (IEntityCollection2)propertyMap.DestinationProperty.GetValue(mappedObject);//newline
                    ec.AddRange((IEntityCollection2)propertyValueToAssign);//newline
                }//newline
            }//newline
        }

Now you can automap entities. You could do it before but u ended up having to map the collections seperately and then add in the entities to the objects collections manually. Which sucked.

Heres a semi-complete example (altered automapper classes not included - see the attachment):


class Program
    {
        static void Main(string[] args)
        {
            Contact c = new Contact();
            c.CreditCards = new List<SalesOrderHeader>();
            c.CreditCards.Add(new SalesOrderHeader() { Comment = "4444333322221111" });


            AutoMapper.Mappers.TypeMapObjectMapperRegistry.AllMappers = () => new AutoMapper.Mappers.ITypeMapObjectMapper[]
            {
                //new AutoMapper.Mappers.TypeMapObjectMapperRegistry.CustomMapperStrategy(),
                //new AutoMapper.Mappers.TypeMapObjectMapperRegistry.NullMappingStrategy(),
                //new AutoMapper.Mappers.TypeMapObjectMapperRegistry.CacheMappingStrategy(),
                new NewObjectPropertyMapMappingStrategy()//,
                //new AutoMapper.Mappers.TypeMapObjectMapperRegistry.ExistingObjectMappingStrategy()
            };

            AutoMapper.Mappers.MapperRegistry.AllMappers = () => new AutoMapper.IObjectMapper[]
        {
            new AutoMapper.Mappers.DataReaderMapper(),
            new AutoMapper.Mappers.TypeMapMapper(AutoMapper.Mappers.TypeMapObjectMapperRegistry.AllMappers()),
            new AutoMapper.Mappers.StringMapper(),
            new AutoMapper.Mappers.FlagsEnumMapper(),
            new AutoMapper.Mappers.EnumMapper(),
            new AutoMapper.Mappers.ArrayMapper(),
            new AutoMapper.Mappers.EnumerableToDictionaryMapper(),
            new AutoMapper.Mappers.DictionaryMapper(),
            new ListSourceMapper(),
            new AutoMapper.Mappers.EnumerableMapper(),
            new AutoMapper.Mappers.AssignableMapper(),
            new AutoMapper.Mappers.TypeConverterMapper(),
            new AutoMapper.Mappers.NullableMapper()
        };

            
            AutoMapper.Mapper.CreateMap<Contact, ContactEntity>()
                .ForMember(p => p.SalesOrderHeader, p => p.MapFrom(x => x.CreditCards))
                ;

            AutoMapper.Mapper.CreateMap<SalesOrderHeader, SalesOrderHeaderEntity>();

            IList foo = new EntityCollection<SalesOrderHeaderEntity>();

            foo.Add(new SalesOrderHeaderEntity());

            var b = AutoMapper.Mapper.Map<Contact, ContactEntity>(c);

            Console.WriteLine(b.SalesOrderHeader.Count);
            Console.WriteLine(b.SalesOrderHeader[0].Comment);
            Console.Read();
        }
    }

    public class Contact
    {
        public List<SalesOrderHeader> CreditCards
        {
            get;
            set;
        }
    }

    public class SalesOrderHeader
    {
        public string Comment
        {
            get;
            set;
        }
    }

Hope this helps someone. Enjoy!

Attachments
Filename File size Added on Approval
AutomapperTest.zip 5,246 02-Mar-2010 08:28.31 Approved
worldspawn avatar
worldspawn
User
Posts: 321
Joined: 26-Aug-2006
# Posted on: 02-Mar-2010 08:30:54   

Um... I didn't need to completely re-implement ListSourceMapper as it is a public class.


public class ListSourceMapper : AutoMapper.Mappers.ListSourceMapper
    {
        protected override IList GetEnumerableFor(object destination)
        {
            if (destination is IList)
                return (IList)destination;
            var listSource = (IListSource)destination;
            return listSource.GetList();
        }
    }

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 02-Mar-2010 10:36:50   

Thanks for sharing simple_smile (and why does it call Clear() on a list source... disappointed )

Frans Bouma | Lead developer LLBLGen Pro
Posts: 134
Joined: 10-Jan-2007
# Posted on: 23-Mar-2010 21:51:09   

Thought I would throw my 2 cents in. First off, I was stuck on the collection issue, so big thank you to Sam for getting that working.

Other issues I ran into:

  • When you use the AutoMappers configuration check (Mapper.AssertConfigurationIsValid()), it will throw errors for all fields of the destination that do not have a match. This causes issues with some of the "extra" entity properties (Validator, AuthorizerToUse, IsDeserializing, AlwaysFetchXX, etc.).
  • There is an issue when setting the PK fields of the self servicing entities and setting IsNew on all entities. Same as the ModelBinder in ASP.NET MVC. This code attempts to fix the above issues (see attachment). Usage:

LLBLGenAutoMapper.Init(); //Must be called before calling Mapper.CreateMap

Mapper.CreateMap<ContactEntity, ContactViewModel>(); //Entity to ViewModel

Mapper.CreateMap<ContactViewModel, ContactEntity>() //ViewModel to Entity
    .ForEntity() //this fixes up the mapping
    .ForMember(dest => dest.MiddleInitial, opt => opt.Ignore()) //AutoMapper method to skip mapping of field
    ;

Mapper.AssertConfigurationIsValid(); //Validate

What .ForEntity() does:

  • Adds a BeforeMap/AfterMap to toggle the IsDeserializing flag so lazy-loading does not occur.
  • Adds an Ignore for all "extra" fields
  • Adds an Ignore for all PK fields, adds a BeforeMap to set the value using ForcedCurrentValueWrite and sets IsNew = false. (The set only happens if the value is a ValidKeyValue (not null/DBNull, numeric > 0, strings not empty, etc.) I have not tested this a lot, but it seems to work well enough for converting to and from.

Brian

Attachments
Filename File size Added on Approval
LLBLGenAutoMapper.cs 5,597 23-Mar-2010 21:51.29 Approved
worldspawn avatar
worldspawn
User
Posts: 321
Joined: 26-Aug-2006
# Posted on: 25-Mar-2010 03:02:26   

Your welcome.

I end up adding a AfterMap on my mapping that does something like:

AfterMap((p,x) => { x.IsNew = p.RegistrationId == 0; })

Does the job. As the for the "extra" properties I either dont' assert simple_smile or end up having to add all the ignores.

Your solution is definately more thought out/reusable. I'll have to steal it simple_smile

worldspawn avatar
worldspawn
User
Posts: 321
Joined: 26-Aug-2006
# Posted on: 28-Apr-2010 03:01:59   

Added some code to your ForEntity to automattically ignore relationship properties. You can pass in a list of properties to NOT ignore. Any properties of type IEntity2 or IEntityCollection2 get ignored. Saves a few headaches when using Assert.

        if (factory is IEntityFactory2)
        {
            var fields = (factory as IEntityFactory2).CreateFields();
            if (pkFields.Length == 0)
                pkList = fields.PrimaryKeyFields.OfType<IEntityFieldCore>();

            relationList = tdest.GetProperties().Where(p => p.PropertyType.GetInterface("IEntity2") != null || p.PropertyType.GetInterface("IEntityCollection2") != null).Select(p => p.Name);
        }

        if (relationList != null)
            foreach (string rel in relationList)
            {
                if (includeRelatedFields != null && includeRelatedFields.Contains(rel))
                    continue;
                map.ForMember(rel, opt => opt.Ignore());
            }
Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 28-Apr-2010 11:55:25   

Thanks for the feedback. simple_smile

Posts: 134
Joined: 10-Jan-2007
# Posted on: 29-Apr-2010 23:27:39   

worldspawn:

Good idea on the ignore. How did you change the call signature to pass in the relationships to include?

Could the check for GetInterface look for IEntityCore and IEntityCollectionCore to work with SelfServicing also?

Brian

Posts: 134
Joined: 10-Jan-2007
# Posted on: 03-May-2010 15:57:03   

I liked the idea so well I decided to go back to my code and add it along with some other methods to make ignoring and mapping fields quicker. So....

Base call with method to IgnoreAllRelations (you can pass a list of IEntityRelation exceptions)


LLBLGenAutoMapper.Init(); //Must be called before calling Mapper.CreateMap

Mapper.CreateMap<ContactEntity, ContactViewModel>(); //Entity to ViewModel

Mapper.CreateMap<ContactViewModel, ContactEntity>() //ViewModel to Entity
    .ForEntity(true) //this fixes up the mapping, true tells it to auto map the PK fields
    .IgnoreAllRelations() //adds an ignore for all relations
    .ForMember(dest => dest.MiddleInitial, opt => opt.Ignore()) //AutoMapper method to skip mapping of field
    ;

Mapper.AssertConfigurationIsValid(); //Validate

MapOnlyFields - to provide just the list of fields you want, you need to pass all fields used, including PK.


     Mapper.CreateMap<ContactViewModel, ContactEntity>() //ViewModel to Entity
    .ForEntity(true)
    .MapOnlyFields(ContactFields.ContactId, ContactFields.FirstName, ContactFields.LastName)

IgnoreFields - ignores the list of fields passed


     Mapper.CreateMap<ContactViewModel, ContactEntity>() //ViewModel to Entity
    .ForEntity(true)
    .IgnoreFields(ContactFields.MiddleInitial, ContactFields.Address1)

Misc Methods: MapKey - to handle a PK (if true is not passed to ForEntity) IgnoreMembers - takes a list of property names as string list and adds an ignore for all.

Brian

Attachments
Filename File size Added on Approval
LLBLGenAutoMapper.cs 9,025 03-May-2010 15:57.29 Approved
worldspawn avatar
worldspawn
User
Posts: 321
Joined: 26-Aug-2006
# Posted on: 13-Jan-2011 03:19:11   

Hi Brian, love the changes.

There was nothing cool about how I was passing the relation properties to ignore. Just a string array with the property names in it