Upgrading from AutoMapper 3->5. Properties of type object, List<object>, etc not being mapped properly at runtime. CreateMap<object,object>.Include()s not working anymore

73 views
Skip to first unread message

ma...@mattseanhull.com

unread,
Dec 3, 2016, 7:56:48 AM12/3/16
to AutoMapper-users
I have been tasked with upgrading our solution from AutoMapper 3.x to the Current 5.x. It's not been an easy transition, but I've been able to iron out most of the issues thus far. The solution is huge and there are a ton of mappings. One of the bigger issues that I am tackling now is that a lot of our system is based on type object (rather than concrete types). This was a design choice made in the system years ago and unfortunately cannot be changed. To better explain what I mean, we have things like this:

public class MyObject {
   
public int Property1 {get;set;}
   
public string Property2 {get;set;}
}

public class OtherObject {
   
public int X {get;set;}
   
public string Y {get;set;}
}

public class SomeOtherObject {
     
public object TheObject {get;set;}  // May be a MyObject, May be an OtherObject
}


we also have "list classes" that are defined like this

public class OurListObject : List<object> {
   
// no custom implementation
   
// Items contained with in may be a MyObject, OtherObject or even SomeOtherObject
}

I agree that this definitely not the best design but it's what we're stuck with. Now on to my exact issue: The above code description is just to give a generalized idea of what exactly goes on in our system. There is a lot of type checking at run time to perform conditional logic on the actual type of things, etc. 

My code really looks like this:

public abstract class Event { // this class is never actually inherited
   
public class LogEvent {  // the only commonality between all these events is that that they inherit object
       
// some properties
   
}

   
public class ErrorEvent {
       
// some different properties
   
}

   
// more event type classes.
}

public class OrderLogEvent : Event.LogEvent { }
public class OrderErrorEvent : Event.LogEvent { }

public class CustomerOrderLogEvent : OrderLogEvent { }
public class CustomerOrderErrorEvent : OrderErrorEvent { }

public class OrderEventList : List<object> { }

public class OrderInfo {
   
public OrderEventList { get;set;}
   
public object MostRecentEvent {get;set; }
}

For the sake of brevity, I'll omit the corresponding "Map-To" classes. But let's assume they are identical in every regard except for their namespace which we'll call NS1 and NS2. Please note that the number of classes is much, much more than this small handful.

In AutoMapper 3 our mappings then looked like this:

Mapper.CreateMap<object,object>
   
.Include<NS1.LogEvent.LogEvent,NS2.Event.LogEvent>()
    .Include<NS1.Event.ErrorEvent,NS2.Event.ErrorEvent>();
    //.Include() for any other of the "Event" types

Mapper.CreateMap<NS1.CustomerOrderLogEvent,NS2.CustomerOrderLogEvent >();
Mapper.CreateMap<NS1.CustomerOrderErrorEvent,NS2.CustomerOrderErrorEvent>();
Mapper.CreateMap<NS1.OrderEventList,NS2.OrderEventList>();
Mapper.CreateMap<NS1.OrderLogEvent,NS2.OrderLogEvent>();
Mapper.CreateMap<NS1.Event.LogEvent,NS2.Event.LogEvent>();
Mapper.CreateMap<NS1.Event.ErrorEvent,NS2.Event.ErrorEvent>();

//etc


During mapping of objects, any of the properties that were of type object would get mapped to the appropriate type based on their run-time type. Furthermore any of the containers that Inherit from List<object> would have their child items converted to the appropriate type based on their run time type. In the example above, if an instance of OrderEventList had 2 items each of those items would be / could me mapped to two separate types.

But in AutoMapper 5...

What happens now in AutoMapper5 is that running the above gives me a exception about not being able to cast to object **. Furthermore its not limited to the types listed above, it happens for all sorts of properties such as string properties or any other random DTO that's not part of the <object,object> includes. This makes sense to me since all classes derive from object. If I remove the object=>object map, then anything that is defined with a property type of object doesn't get mapped. Further, NS1.OrderEventList would get mapped to NS2.OrderEventList but because its a List<object> the contained items don't get converted to NS2 but stay as-is, unconverted ... which doesn't throw an exception or anything because they are of type object. We end up with a mix of NS1 collections with NS2 sub-items that didn't get mapped properly. I also tried removing the Includes on object->object and instead using IncludeBase<object,object>() on the derived types. This didn't seem to work either.


What I've tried to do is this **:
  1. Create a static dictionary to contain Type->Type mappings
  2. Add an extension method to "Register" these types for their mappings into the dictionary (which then needs to be added for each map created)
  3. ForAllPropertyMaps, where DestinationPropertyType == object and SourceType == object, register a custom MemberValueResolver 
  4. The MemberValueResolver would then inspect the run time type of the SourceMember, query the static dictionary for a match and if found call Mapper.Map( ... ) supplying the source/destination types directly
  5. If a direct match isn't found, test if any of the items in the dictionary are "assignable" (ie derived) and use that mapping instead
    • I haven't worked this out completely because I'm sure there's got to be a better way so put it off to the side for the moment

Now this works (somewhat, see below). But it seems overly clunky to have to go to each one of my types and call my Register() method. Especially when the number of types is far more than displayed above. I also don't like having to inspect the types myself if there is some other better way of doing it. The performance is most likely going to be killed by trying to do this, even if I cache the resultant type (related to at least step 5,if step 4 fails)

The problem I have is that this works for properties of type object only. If my property is instead one of the list-type objects (such as OrderEventList), the algorithm above doesn't work. So to side step that, I added another ForAllPropertyMaps, this type one that checked if the target/source were derived from List<object> and if so call another MemberValueResolver. That resolver would create a new List of the output type and then iterate the source. For each object in the source, delegate creation to an instance of the MemberValueResolver used for object->object 

Now it works. Kind of! If I need to Map an OrderEventList (or any of the other List<object> derived classes) directly, the child/contained objects remain in their original state (still stuck in NS1). I would need to register a custom resolver or converter for each custom List<object> type. There's a lot of them and this would be horrible to implement.

I realize that the design of our project is horrible. Everyone knows it and it's been like that for years. If I could change everything to use a common base class or interface, and remove all the types that derive from List<object> (no idea who that was ever a good idea over composition) and all the properties of type object out of the project I would. But we can't. 

What are my options here? Am I missing something or is this just not possible in AutoMapper 5?  If we need to stick with Automapper 3 I guess we will have to, but we've already done so much trying to migrate I'd hate to throw all that work out.

** PS. It's Saturday morning and I am at home. I wrote all of the above from memory and if anything needs to be clarified please let me know. I left out the specifics of my "Register()"+custom resolvers because I don't have access to my code base at the moment, but if need be I can get to it or supply it on Monday. However I'm really hoping that there is some better solution that wouldn't include this to begin with.

Jimmy Bogard

unread,
Dec 7, 2016, 11:41:27 PM12/7/16
to automapper-users
Can you open a GH issue for this? The formatting is kinda messed up.

--
You received this message because you are subscribed to the Google Groups "AutoMapper-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to automapper-users+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages