Have you ever heard or used AutoMapper? What a question, of course you have. And in the very unlikely scenario that you haven't, it's the object to object mapper that allows you to map probably everything. In short no more manual, boring, tedious, error-prone mapping.
However, the great power comes with great responsibility. In the recent time, I had an occasion to fix 2 difficult to track bugs related to improper usage of AutoMapper. Both issues were related to the feature of AutoMapper which according to me is almost useless and at least should be disabled by default. Let's look at the following 2 classes and testing code:
public class SomeSourceClass { public Guid Id { get; set; } public string IdAsString => Id.ToString(); public string Value { get; set; } } public class SomeDestinationClass { public Guid Id { get; set; } public string IdAsString => Id.ToString(); public string Value { get; set; } } class Program { static void Main() { Mapper.Initialize(config => config.CreateMap<SomeSourceClass,SomeDestinationClass>>()); var src = new SomeSourceClass { Id = Guid.NewGuid(), Value = "Hello" }; var dest = Mapper.Map<SomeDestinationClass>(src); Console.WriteLine($"Id = {dest.Id}"); Console.WriteLine($"IdAsString = {dest.IdAsString}"); Console.WriteLine($"Value = {dest.Value}"); } }This works as a charm. If you run this example, you should see output like that:
Id = a2648b9e-60be-4fcc-9968-12a20448daf4
IdAsString = a2648b9e-60be-4fcc-9968-12a20448daf4
Value = Hello
Now, let's introduce interfaces that will be implemented by SomeSourceClass and SomeDestinationClass:
public interface ISomeSourceInterface { Guid Id { get; set; } string IdAsString { get; } string Value { get; set; } } public interface ISomeDestinationInterface { Guid Id { get; set; } string IdAsString { get; } string Value { get; set; } } public class SomeSourceClass: ISomeSourceInterface { /*... */} public class SomeDestinationClass : ISomeDestinationInterface { /*... */}We also want to support mappings from ISomeSourceInterface to ISomeDestinationInterface so we need to configure AutoMapper accordingly. Otherwise the mapper will throw an exception.
Mapper.Initialize(config => { config.CreateMap<SomeSourceClass, SomeDestinationClass>(); config.CreateMap<ISomeSourceInterface, ISomeDestinationInterface>(); }); var src = new SomeSourceClass { Id = Guid.NewGuid(), Value = "Hello" }; var dest = Mapper.Map<ISomeDestinationInterface>(src); Console.WriteLine($"Id = {dest.Id}"); Console.WriteLine($"IdAsString = {dest.IdAsString}"); Console.WriteLine($"Value = {dest.Value}");If you run this code, it'll seemingly work as the charm. However, there is a BIG PROBLEM here. Let's examine more carefully what was written to the console. The result will look as follows:
Id = a2648b9e-60be-4fcc-9968-12a20448daf4
IdAsString =
Value = Hello
Do you see a problem? The readonly property IdAsString is empty. It seems crazy because IdAsString property only returns the value of Id property which is set. How is it possible?
And here we come the feature of AutoMapper which according to be should be disabled by default i.e. automatic proxy generation. When AutoMapper tries to map ISomeSourceInterface to ISomeDestinationInterface it doesn't know which implementation of ISomeDestinationInterface should be used. Well, actually no implementation may even exists, so it generates one. If we check the type of dest property we'll see something like:
Proxy<ConsoleApplication1.ISomeDestinationInterface_ConsoleApplication1_Version=1.0.0.0_Culture=neutral_PublicKeyToken=null>.
Initially this function may look as something extremely useful. But it's the Evil at least because of the following reasons:
- As in the example, the mapping succeeds but the result object contains wrong data. Then this object may be used to create other objects... This can lead to really difficult to detect bugs.
- If a destination interface defines some methods, a proxy will be generated, but the mapping will fail due to System.TypeLoadException.
- It shouldn't be needed in the well written code. However, if you try to cast the result of the mapping to the class, then System.InvalidCastException exception will be thrown.
The final configuration looks as follows. It's also worth mentioning that in this case we actually don't need to define mapping from SomeSourceClass to SomeDestinationClass. AutoMapper is clever enough to figure out that these classes implements interfaces.
Mapper.Initialize( config => { config.CreateMap<ISomeSourceInterface, ISomeDestinationInterface>().As<SomeDestinationClass>(); });
AutoMapper proxy generation feature is the Evil.
*The picture at the beginning of the post comes from own resources and shows Okonomiyaki that we ate in Hiroshima. One of the best food we've ever eaten.