- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
AutoMapper & EntityCollection<T> properties
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!
| Filename | File size | Added on | Approval |
|---|---|---|---|
| AutomapperTest.zip | 5,246 | 02-Mar-2010 08:28.31 | Approved |
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();
}
}
Joined: 10-Jan-2007
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
| Filename | File size | Added on | Approval |
|---|---|---|---|
| LLBLGenAutoMapper.cs | 5,597 | 23-Mar-2010 21:51.29 | Approved |
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
or end up having to add all the ignores.
Your solution is definately more thought out/reusable. I'll have to steal it
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());
}
Joined: 10-Jan-2007
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
Joined: 10-Jan-2007
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
| Filename | File size | Added on | Approval |
|---|---|---|---|
| LLBLGenAutoMapper.cs | 9,025 | 03-May-2010 15:57.29 | Approved |
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
)