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 ideal solution would be to disable this feature. However, I don't know how :( The workaround is to explicitly tell AutoMapper not to generate proxies. To do that we need to use
As method and specify which concrete type should be created instead of a proxy.
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.