I've been working on a project that is exposed via a service layer. Trying to "do the right thing", I've created a DTO project that contains everything that is sent over the wire from the service layer (which happens to be ServiceStack). I really detest writing mapping code, so I'm using AutoMapper to take care of that for me.
I ran into an issue though - some of my entities are polymorphic. That is, I have an abstract base class and two inheritors of that base class. And I didn't want my DTOs to be polymorphic - I wanted them flat. I solved the problem by creating two methods and an interface.
Both methods require that the DTO inherit from this interface.public interface IPolymorphicDto { string Type { get; set; } }
Here's the first method, which takes polymorphic objects and maps them to a flat DTO.
public static void CreatePolymorphicToFlatMap< TSourceBase, TSourceInheritorA, TSourceInheritorB, TDestination>() where TSourceInheritorA : TSourceBase where TSourceInheritorB : TSourceBase { // Map the abstract source type to the destination DTO // and include the two inheritor source types. var mappingExpression = Mapper.CreateMap<TSourceBase, TDestination>() .Include<TSourceInheritorA, TDestination>() .Include<TSourceInheritorB, TDestination>(); // Tell AutoMapper to ignore each property type that exists // in either inheritor but not the abstract base. (we'll // take care of them later) foreach (var name in typeof(TSourceInheritorA).GetProperties() .Where(p => p.DeclaringType == typeof(TSourceInheritorA)) .Select(p => p.Name) .Union(typeof(TSourceInheritorB).GetProperties() .Where(p => p.DeclaringType == typeof(TSourceInheritorB)) .Select(p => p.Name))) { mappingExpression.ForMember(name, options => options.Ignore()); } // Tell AutoMapper how to map the inheritor's properties. Mapper.CreateMap<TSourceInheritorA, TDestination>(); Mapper.CreateMap<TSourceInheritorB, TDestination>(); }The second method takes a flattened DTO and maps it into a polymorphic objects.
public static void CreateFlatToPolymorphicMap< TSource, TDestinationBase, TDestinationInheritorA, TDestinationInheritorB>() where TSource : IPolymorphicDto where TDestinationBase : class where TDestinationInheritorA : TDestinationBase, new() where TDestinationInheritorB : TDestinationBase, new() { Mapper.CreateMap<TSource, TDestinationBase>() // We can't rely on AutoMapper to create an instance // of TDestinationBase, since it's abstract. So we // need to tell it how to do so. .ConstructUsing(source => { // We'll use a bit of reflection to create the // instance. return (TDestinationBase) Activator.CreateInstance( Type.GetType(source.Type)); }) // We also can't rely on AutoMapper to figure out // how to map the properties of the inheritors. // We're creating a TDestinationBase map here, so // AutoMapper won't know how to map the properties // that exist only in the inheritors. We'll solve // the problem by checking the Type property of the // source object (note that we put a generic constraint // on TSource - it must be a IPolymorphicDto). If the // Type property of TSource matches the full name of // TDestinationInheritorA, then return whatever // AutoMapper maps to when we tell it we're mapping to // TDestinationInheritorA. Otherwise, have Automapper // map to TDestinationInheritorB. .ConvertUsing(source => source.Type == typeof(TDestinationInheritorA).FullName ? (TDestinationBase) Mapper.Map<TDestinationInheritorA>(source) : (TDestinationBase) Mapper.Map<TDestinationInheritorB>(source)); // Finally, we need to tell AutoMapper how to map to the // inheritors. Mapper.CreateMap<TSource, TDestinationInheritorA>(); Mapper.CreateMap<TSource, TDestinationInheritorB>(); }So now we have our methods. Let's define some classes to map.
//Domain Objects public class Customer { private readonly List<Order> _orders = new List<Order>(); public List<Order> Orders { get { return _orders; } } } public abstract class Order { public int Id { get; set; } } public class OnlineOrder : Order { public string Referrer { get; set; } } public class MailOrder : Order { public string City { get; set; } } //Dtos public class CustomerDto { private readonly List<OrderDto> _orders = new List<OrderDto>(); public List<OrderDto> Orders { get { return _orders; } } } public class OrderDto : IPolymorphicDto { public int Id { get; set; } public string Type { get; set; } public string Referrer { get; set; } public string City { get; set; } }And now we can set up our mappings.
Mapper.CreateMap<Customer, CustomerDto>(); CreatePolymorphicToFlatMap< Order, OnlineOrder, MailOrder, OrderDto>(); Mapper.CreateMap<CustomerDto, Customer>(); CreateFlatToPolymorphicMap< OrderDto, Order, OnlineOrder, MailOrder>(); // Ask AutoMapper to validate our mapping. If something is // wrong, this will throw an exception. Mapper.AssertConfigurationIsValid();Finally, we can map back and forth with ease.
Customer customer = new Customer(); Order onlineOrder = new OnlineOrder { Id = 1, Referrer = "google" }; Order mailOrder = new MailOrder { Id = 2, City = "Detroit" }; customer.Orders.Add(onlineOrder); customer.Orders.Add(mailOrder); // customer will have two Orders: one of type OnlineOrder, with // an Id of 1 and a Referrer of "google"; and one of type // MailOrder, with an Id of 2 and a City of "Detroit"; var mapped = Mapper.Map<CustomerDto>(customer); // mapped will have two Orders: one with an Id of 1, a Type of // OnlineOrder, a Referrer of "google", and a City with a null // value; and one with an Id of 2, a Type of MailOrder, a // Referrer with a null value, and a City of "Detroit". var mappedBack = Mapper.Map<Customer>(mapped); // mappedBack will be identical to the original customer.
No comments:
Post a Comment