C# generics based object to object mapper question
I have the need for an object to object mapper in my application. I've tried out a few, but haven't been able to find anything that fits my needs, so I'm writing my own. Currently I have an interface like below:
public interface IMapper<T, R> {
T Map(R obj);
}
I then implement an AccountMapper that maps a Customer to an Account as:
public class AccountMapper : IMapper<Account, Customer> {
Account Map(Customer obj) {
// mapping code
}
}
This works fine so far, however I have several source entities that map to the same destination entity. For instance I have a Payment and an Invoice that both map to BillHistory. For the above to support this, I need to make two separate mappers (ie. BillHistoryPaymentMapper and BillHistoryInvoiceMapper), which is fine. However, I'd love to be able to implement it slightly differently like below. Only problem is I don't know if it's possible and if so, I don't know the correct syntax.
public interface IMapper<T> {
T Map<R>(R obj);
}
public class BillHistoryMapper : IMapper<Account> {
public BillHistory Map<Invoice>(Invoice obj) {
// mapping code
}
public BillHistory Map<Payment>(Payment obj) {
// mapping code
}
}
While the first implementation works fine, the second would be slightly more elegant. Is this possible and if so what would the correct syntax look like?
edit-------
I hate when people do this, but of course I forgot to mention one little detail. We have an abstract class between the mapper and the interface to impleme开发者_C百科nt some common logic across all of the mappers. So my mapper signature is actually:
public class BillHistoryMapper : Mapper<BillHistory, Invoice> {
}
where Mapper contains:
public abstract class Mapper<T, R> : IMapper<T, R> {
public IList<T> Map(IList<R> objList) {
return objList.ToList<R>().ConvertAll<T>(new Converter<T, R>(Map));
}
}
You'll have to use your first interface and implement the interface multiple times on your object:
public class BillHistoryMapper : IMapper<Account, Invoice>,
IMapper<Account, Payment> {
...
}
I would serious consider taking a look at AutoMapper instead of writing your own. There are a lot of nuances in mapping that it has already solved, not to mention it has been through plenty of performance testing, bug fixes, etc.
With regard to your abstract class consider getting rid of it and replacing it with an extension method. This will allow you to use the MapAll
function regardless of whether you implement the interface or use some sort of inheritance chain.
public static class MapperExtensions
{
public static IEnumerable<TOutput> MapAll<TInput, TOutput>
(this IMapper<TInput, TOutput> mapper, IEnumerable<TInput> input)
{
return input.Select(x => mapper.Map(x));
}
}
This will now make it easier when trying to solve your problem above because you no longer have to inherit from a base class you can now implement the mapping interface for the types you want to map.
public class BillHistoryMapper :
IMapper<Invoice, BillHistory>, IMapper<Payment, BillHistory>
{
public BillHistory Map<Invoice>(Invoice obj) {}
public BillHistory Map<Payment>(Payment obj) {}
}
Also consider changing your IMapper
generic parameters to be the other way round (I took the liberty in the previous examples):
public interface IMapper<in TInput, out TOutput>
{
TOutput Map(TInput input);
}
The reason for this is that it directly maps to the System.Converter<T>
delegate and you can do something like:
IMapper<ObjectA, ObjectB> myAToBMapper = new MyAToBMapper();
ObjectA[] aArray = { new ObjectA(), new ObjectA() };
ObjectB[] bArray = Array.ConvertAll<ObjectA, ObjectB>(aArray, myAToBMapper.Map);
List<ObjectA> aList = new List<ObjectA> { new ObjectA(), new ObjectA() };
List<ObjectB> bList = aList.ConvertAll<ObjectB>(myAToBMapper.Map);
// Or
var aToBConverter = new Converter<ObjectA, ObjectB>(myAToBMapper.Map);
bArray = Array.ConvertAll(aArray, aToBConverter);
bList = aList.ConvertAll(aToBConverter);
AutoMapper has also been suggested which will make your life easier. However if you wanted to keep your mapping abstraction and have your code agnostic to your mapping strategy then it is very easy to use the above interface to inject a wrapper around AutoMapper. It will also mean you can continue to use the MapAll
extension method explained above.
public class AutoMapperWrapper<in TInput, out TOutput> : IMapper<TInput, TOutput>
{
public TOutput Map(TInput input)
{
return Mapper.Map<TOutput>(input);
}
}
Final word
Also keep in mind you are not always going to find that your mapping strategy will work across the board so don't try to fight your domain and force it to fit your mapping strategy. One particular example is you might have to map from two input items into one. You can obviously make this fit your strategy but you may find it becomes messy. In this particular example consider it a merge.
Your second example will work with only a few changes:
// you have to include the R type in the declaration of the Mapper interface
public interface IMapper<T, R> {
T Map<R>(R obj);
}
// You have to specify both IMapper implementations in the declaration
public class BillHistoryMapper : IMapper<Account, Invoice>, IMapper<Account, Payment> {
public BillHistory Map<Invoice>(Invoice obj) {
// mapping code
}
public BillHistory Map<Payment>(Payment obj) {
// mapping code
}
}
I'm not sure if this actually gains you anything over the existing pattern, though.
If there is a limited number of types you want to map from, then I would use the first method of declaring the input and output types in the interface definition. Then a mapper can implement interfaces for each input type it supports, so your BillHistoryMapper
would be declared as:
public class BillHistoryMapper : IMapper<BillHistory, Invoice>, IMapper<BillHistory, Payment>
{
...
}
精彩评论