AutoMapper: Incorrect mapping in case of multiple interfaces

Automapper version 4.2.1 In case when source implements more than one interface and we do not have mapping configuration for the source but have it for interfaces it will be impossible to use mapping configured for second (third, fourth…) interface

        public interface IChildA
        {
            string PropertyA { get; set; }
        }

        public interface IChildB
        {
            string PropertyB { get; set; }
        }

        public class Source : IChildA, IChildB
        {
            public string PropertyA { get; set; }

            public string PropertyB { get; set; }
        }

        public class Destination
        {
            public string PropertyA { get; set; }

            public string PropertyB { get; set; }
        }
...

        public void MappingTest()
        {
            var conf = new MapperConfiguration(
                config =>
                {
                    config.CreateMap<IChildA, Destination>();
                    config.CreateMap<IChildB, Destination>();
                });
            var mapper = conf.CreateMapper();

            var src = new Source { PropertyA = "A", PropertyB = "B" };
            var dest = new Destination();
            mapper.Map<IChildA, Destination>(src, dest); // will map PropertyA
            mapper.Map<IChildB, Destination>(src, dest); // will NOT map PropertyB

            Trace.WriteLine(dest.PropertyA == src.PropertyA); // true
            Trace.WriteLine(dest.PropertyB == src.PropertyB); // false !!!
        }

The code above will not fill PropertyB (It will in case we will change order of interfaces in the Source class declaration)

The reason is MapperConfiguration.ResolveTypeMap method:

        public TypeMap ResolveTypeMap(object source, object destination, Type sourceType, Type destinationType)
        {
            return ResolveTypeMap(source?.GetType() ?? sourceType, destination?.GetType() ?? destinationType);
        }

It will call source.GetType() and then will look for the first known mapping (it will be IChildA in our case), so “sourceType” (IChildB in our case) will be ignored and we will got invalid mapping.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 26 (13 by maintainers)

Most upvoted comments

Just a heads up that I’m still on this thing, just havn’t found any time to actually write the code

A lot of stuff went on here while I was sleeping 😃

@oddbear you’re right about the order of interfaces, as I said, it was just a quick thought. I like your approach as it eliminates the dependency on ValueTuple.

I have some thoughts on this issue, or at least some ways I’d like to see it implemented. I think I might be able to conceive most of it through some extension methods, but I’d really have to bunker down and hammer out the code to be sure. Without changing the core it might have some uglyish parts.

I was thinking of having something like a MapAs method taking one or more type parameters to use explicitly. Having a separate method should enhance readability. @jbogard, what do you mean with a global config? That doesn’t sound very useful, as in some case one might still want to use the default behaviour.

Anyways, more on this when I got down with some code. I don’t think I’ll have much time for it today, but maybe I could put together a little POC by tomorrow or thursday.

With something like this, where it’s not a bug but a feature request, I’m open to PRs. I wouldn’t use this functionality on my projects, which means for me its priority is zero. If someone can get it to work without breaking existing functionality, I’m happy to entertain a PR.

Quick thought on a workaround. @oddbear 's idea about proxies got me thinking.

I don’t want to create a proxy for every possible interface combo, so I wondered: can this be fixed using generics? Turns out this is easily achieved using the magic of ValueTuple.

Consider the following map:

CreateMap<ValueTuple<IHasId, IHasEntityType>, Entity>()
    .ForMember(
        dest => dest.id,
        src => src.MapFrom(x => x.Item1.id)
    )
    .ForMember(
        dest => dest.type,
        src => src.MapFrom(x => x.Item2.GetEntityType());
    )

I can now easily map my model as such:

mapper.Map<Entity>(
    (model as IHasId, model as IHasEntityType)
);

Of course you’ll still need to specify the different mapping combo’s, but at least you won’t have a bunch of singular-use proxies cluttering up your project.

Given this any more thoughts?

We have the same type of issues.

There is so much strange behavior on this scenario. Interface order on the class picks the mapping profile, and this might change based on the source object type.

The only workaround I have found is to use mapping to a Proxy in between. That is, not map A to B, but A to proxy of interface, to instance of B:

//Automapper 7.0.1
void Main()
{
    Mapper.Initialize(config =>
    {
        //Ordering has no impact here:
        config.CreateMap<IFly, IFly>();
        config.CreateMap<ISwim, ISwim>();
        config.CreateMap<IWalk, IWalk>();
    });

    //Does not throw any exception.
    Mapper.AssertConfigurationIsValid();
    
    var input = new Message {
        FinLenght = 1,
        WingLength = 2,
        LegLenght = 3,
        DoNotMap = 4
    };

    DoWork(input)
        .Dump("Using Workaround Methods");
    //WingLength: 2 
    //FinLenght: 1
    //LegLenght: 3
    //DoNotMap: -1
}

MyAnimal DoWork(object input)
{
    var output = new MyAnimal
    {
        FinLenght = -1,
        DoNotMap = -1,
        LegLenght = -1,
        WingLength = -1
    };

    void Hack<T>(T iface)
    {
        //Creates Proxy class of interface (must have CreateMap of all, or DoNotMap is also mapped).
        var hack = Mapper.Map<T>(iface);
        Mapper.Map(hack, output);
    }
    
    if (input is IFly fly)
    {
        Hack(fly);
    }

    if (input is ISwim swim)
    {
        Hack(swim); //Maps IFly
    }

    //No CreateMap for this one still picks IFly:
    if (input is IWalk walk)
    {
        Hack(walk); //Maps IFly
    }

    return output;
}

//If MemberList.Source, then ISwim will be used instead of IFly (source vs destination order)
public class Message : ISwim, IFly, IWalk
{
    public int WingLength { get; set; }
    public int FinLenght { get; set; }
    public int LegLenght { get; set; }
    public int DoNotMap { get; set; }
}

//Order of interfaces on destination chooses mapping profile used.
// - if we switch the placement, the result will be different.
// - if we remove CreateMap, of first interface, mapping will be different.
public class MyAnimal : IFly, IWalk, ISwim
{
    public int WingLength { get; set; }
    public int FinLenght { get; set; }
    public int LegLenght { get; set; }
    public int DoNotMap { get; set; }
}

public interface IFly
{
    int WingLength { get; set; }
}

public interface ISwim
{
    int FinLenght { get; set; }
}

public interface IWalk
{
    int LegLenght { get; set; }
}

I will add one more example

Domain:


    public interface IChildA
    {
        string PropertyA { get; set; }
    }

    public interface IChildB
    {
        string PropertyB { get; set; }
    }

    public class Source_A_B : IChildA, IChildB
    {
        public string PropertyA { get; set; }

        public string PropertyB { get; set; }
    }

    public class Source_B_A : IChildB, IChildA // changed order of interfaces
    {
        public string PropertyA { get; set; }

        public string PropertyB { get; set; }
    }    

    public class Destination
    {
        public string PropertyA { get; set; }

        public string PropertyB { get; set; }
    }

Test:

    var conf = new MapperConfiguration(
        config =>
        {
            config.CreateMap<IChildA, Destination>()
                .ForMember(d => d.PropertyA, opt => opt.MapFrom(s => s.PropertyA))
                .ForMember(d => d.PropertyB, opt => opt.Ignore());

            config.CreateMap<IChildB, Destination>()
                .ForMember(d => d.PropertyB, opt => opt.MapFrom(s => s.PropertyB))
                .ForMember(d => d.PropertyA, opt => opt.Ignore());
        });
    conf.AssertConfigurationIsValid();

    var mapper = conf.CreateMapper();

    var srcAB = new Source_A_B { PropertyA = "A", PropertyB = "B" };
    var dest1 = new Destination();
    mapper.Map<IChildA, Destination>(srcAB, dest1); // will map PropertyA
    mapper.Map<IChildB, Destination>(srcAB, dest1); // will NOT map PropertyB !!!

    Console.WriteLine(dest1.PropertyA == srcAB.PropertyA); // true
    Console.WriteLine(dest1.PropertyB == srcAB.PropertyB); // false !!!


    var srcBA = new Source_B_A { PropertyA = "A", PropertyB = "B" };
    var dest2 = new Destination();
    mapper.Map<IChildA, Destination>(srcBA, dest2); // will NOT map PropertyA !!!
    mapper.Map<IChildB, Destination>(srcBA, dest2); // will map PropertyB

    Console.WriteLine(dest2.PropertyA == srcBA.PropertyA); // false !!!
    Console.WriteLine(dest2.PropertyB == srcBA.PropertyB); // true

So mapping strategy depends from order of interfaces (!!!). Source_A_B mapped one way while Source_B_A mapped another. From my point of view it is a mistake to have a code dependent from interface ordering.