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.