Tuesday, January 29, 2013

Flattening Polymorphism with AutoMapper

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