Skip to content

Mutators vs AutoMapper

Anton Chaplygin edited this page Sep 30, 2019 · 1 revision

Interface

AutoMapper

AutoMapper was conceived as a tool that automatically infers mapping rules in most cases. In complex cases it allows to set rules manually:

public class Source
{
    public string UserName { get; set; }
    public SourceItem[] Array { get; set; }
}

public class SourceItem
{
    public int Value { get; set; }
}

public class Dest
{
    public string Login { get; set; }
    public DestItem[] Array { get; set; }
}

public class DestItem
{
    public int Value { get; set; }
}

...

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Source, Dest>()
        .ForMember(d => d.Login, opt => opt.MapFrom(s => s.UserName));
    cfg.CreateMap<SourceItem, DestItem>();
});

In the example above the AutoMapper infers that Array property should be mapped to the corresponding array in the Dest class (Value to Value). We only should instruct it to automatically infer mapping rules for SourceItem and DestItem.

When property names are considerably different the AutoMapper fails to infer the rule, but it allows to set a rule explicitly. In the example above we specify that the UserName property should be mapped to the Login property.

Mutators

All mapping (conversion) rules should be set explicitly.

configurator.Target(dest => dest.Name).Set(source => source.Name);

In the case of nested classes you should provide full paths to properties:

configurator.Target(dest => dest.User.Name).Set(source => source.UserInfo.Login);

If you need to map an array, you can use Each and Current methods:

configurator.Target(dest => dest.Array.Each().Value).Set(source => source.Array.Current().Value);

The Set method can receive almost any correct C# expression:

configurator.Target(dest => dest.Array.Each().Value)
            .Set(source => source.Array.Where(x => x.IntValue == 5).Current().Value);

configurator.Target(dest => dest.StringArray.Each().Value)
            .Set(source => new []{"x", "y", "z"}.Current());

A conversion rule can be set for a concrete array item:

configurator.Target(dest => dest.IntArray[0]).Set(source => source.IntValue);

Multiple mappings

public class Source
{
    public Tuple<string, string> First { get; set; }
    public Tuple<string, string> Second { get; set; }
}

public class Dest
{
    public KeyValuePair<string, string> A { get; set; }
    public KeyValiePair<string, string> B { get; set; }
}

Suppose we want to map First => A and Second => B. But in the first case we need to map Item1 => Key and Item2 => Value, in the second case we need to map Item1 => Value and Item2 => Key.

In the AutoMapper's paradigm mapping rules exist in a context of two types for which we define a mapping. It means we can define a mapping between Tuple<string, string> and KeyValuePair<string, string> for the first case. But there is no way to define another mapping between theese types for the second case.

However, this is not a problem for Mutators:

configurator.Target(dest => dest.A.Key).Set(source => source.First.Item1);
configurator.Target(dest => dest.A.Value).Set(source => source.First.Item2);

configurator.Target(dest => dest.B.Key).Set(source => source.Second.Item2);
configurator.Target(dest => dest.B.Value).Set(source => source.Second.Item1);

Type checking

AutoMapper

Type checking happens in runtime. The AutoMapper can figure out type cast for simple cases, but it crashes in runtime when it can't.

var config = new MapperConfiguration(cfg => cfg.CreateMap<Source, Dest>()
    .ForMember(d => d.IntValue, opt => opt.MapFrom(s => s.StringValue))
);

Mutators

Type checking happes in compile time:

configurator.Target(x => x.IntValue).Set(y => y.StringValue); // compile error
configurator.Target(x => x.IntValue).Set(y => int.Parse(y.StringValue)); // ok

Conditional mapping

Both Mutators and AutoMapper allow to set conditional mapping rules.

var valueConfigurator = configurator.Target(dest => dest.Value);

valueConfigurator.If(source => source.TakeFromX)
                 .Set(source => source.X);

valueConfigurator.If(source => !source.TakeFromX)
                 .Target(dest => dest.Y);

The same construction can be written usign AutoMapper, but it does not work as you expect:

config.CreateMap<Source, Dest>()
      .ForMember(d => d.Value, opt =>
      {
          opt.Condition(s => s.TakeFromX);
          opt.MapFrom(s => s.X);
      })
      .ForMember(d => d.Value, opt =>
      {
          opt.Condition(s => !s.TakeFromX);
          opt.MapFrom(s => s.Y);
      });

It turns out only the last mapping rule for a single member will be applied, so no conversion will happen in case when s.TakeFromX is true.

Summary

  • Mutators interface is more fluent, less nested and less stuffed with generic type arguments.

  • The AutoMapper is more about mapping between pairs of types. Mutators are more about document structure. You cannot define more than one ruleset for a pair of types using the AutoMapper.

  • Mutators' interface forces compile time type checking.

  • Mutators allow to set multiple conditional mappings for the same member of a type.

  • Mutators provide additional features like document validation and path migration.