Automapper does not map properly null List member, when the condition != null is specified
Asked Answered
W

2

6

There is a problem when I try to map a null list (member) of an object, considering that I specified:

.ForAllMembers(opts => opts.Condition((src, dest, srcMember) =>
    srcMember != null
));
cfg.AllowNullCollections = true; // didn't help also

short example from code:

gi.PersonList = new List<Person>();
gi.PersonList.Add(new Person { Num = 1, Name = "John", Surname = "Scott" });
GeneralInfo gi2 = new GeneralInfo();
gi2.Qty = 3;

Mapper.Map<GeneralInfo, GeneralInfo>(gi2, gi);

gi.PersonList.Count = 0, how to fix that?

using System;
using System.Collections.Generic;
using AutoMapper;

public class Program
{
   public static void Main(string[] args)
    {
        Mapper.Initialize(cfg =>
        {
            cfg.AllowNullCollections = true;
            cfg.CreateMap<GeneralInfo, GeneralInfo>()
            .ForAllMembers(opts => opts.Condition((src, dest, srcMember) =>
                srcMember != null
            ));

        });
        GeneralInfo gi = new GeneralInfo();
        gi.Descr = "Test";
        gi.Dt = DateTime.Now;
        gi.Qty = 1;
        gi.PersonList = new List<Person>();
        gi.PersonList.Add(new Person { Num = 1, Name = "John", Surname = "Scott" });

        GeneralInfo gi2 = new GeneralInfo();
        gi2.Qty = 3;

        Console.WriteLine("Count antes de mapeo = " + gi.PersonList.Count);

        Mapper.Map<GeneralInfo, GeneralInfo>(gi2, gi);

        Console.WriteLine("Count despues de mapeo = " + gi.PersonList.Count);
        // Error : gi.PersonList.Count == 0 !!!! 
        //por que? si arriba esta: Condition((src, dest, srcMember) => srcMember != null ...

    }
}

class Person
{
    public int Num { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

class GeneralInfo
{
    public int? Qty { get; set; }
    public DateTime? Dt { get; set; }
    public string Descr { get; set; }
    public List<Person> PersonList { get; set; }
}

https://dotnetfiddle.net/N8fyJh

Winnah answered 15/12, 2017 at 15:5 Comment(1)
Do you really need ForAllMembers here or could you just target PersonList directly?Chromous
O
7

This should work but I'm not sure if you want to micro manage it like that:

cfg.AllowNullCollections = true;
cfg.CreateMap<GeneralInfo, GeneralInfo>()
    .ForMember(x => x.PersonList, opts => opts.PreCondition((src) => src.PersonList != null));

Problem is the collections that are handled specifically (that's true for most mappers though AutoMapper is a bit weird in this case, it's not my favorite) and seem to require the destination collection to be initialized. As I can see, collections are not copied in entirety which makes sense, but you need to initialize and copy individual items (this is my deduction but does sound right).

I.e. even if you skip the source, destination would still end up reinitialized (empty).

Problem as it seems is the Condition which is, given their documentation, applied at some later point, at which time the destination has already been initialized.

PreCondition on the other hand has a different signature to be used like you intended, as it doesn't take actual values, just source is available.

The only solution that seems to work is to use "per member" PreCondition (like the above).


EDIT:
...or this (using the ForAllMembers), but a bit ugly, reflection etc.

cfg.CreateMap<GeneralInfo, GeneralInfo>()
.ForAllMembers(opts =>
    {
        opts.PreCondition((src, context) =>
        {
            // we can do this as you have a mapping in between the same types and no special handling
            // (i.e. destination member is the same as the source property)
            var property = opts.DestinationMember as System.Reflection.PropertyInfo;
            if (property == null) throw new InvalidOperationException();
            var value = property.GetValue(src);
            return value != null;
        });
    }
);

...but there doesn't seem to be any cleaner support for this.


EDIT (BUG & FINAL THOUGHTS):

Conditional mapping to existing collection doesn't work from version 5.2.0 #1918

As pointed out in the comment (by @LucianBargaoanu), this seems to be a bug really, as it's inconsistent in this 'corner' case (though I wouldn't agree on that, it's a pretty typical scenario) when mapping collections and passing the destination. And it pretty much renders the Condition useless in this case as the destination is already initialized/cleared.

The only solution indeed seems to be the PreCondition (but it has issues given the different signature, I'm personally not sure why they don't pass the same plethora of parameters into the PreCondition as well?).

And some more info here:

the relevant code (I think)

nest collection is clear when using Condition but not Ignore #1940

Collection property on destination object is overwritten despite Condition() returning false #2111

Null source collection emptying destination collection #2031

Osugi answered 16/12, 2017 at 20:47 Comment(2)
Yes, other than a PreCondition, there's nothing to do about this one. The details are here.Adamandeve
@LucianBargaoanu thanks - that's the relevant resolution for this issue, bug really, I've edited in that and some additional info.Osugi
P
1

Try like this;

gi = Mapper.Map<GeneralInfo, GeneralInfo>(gi2);

I encounterted with this problem recently and somehow if the destination and sources are same type, the destination parameter doesn't work as expected.

Also, I want to notify that it isn't relevant with just Collection objects. If you debug the gi object you will see that the other properties are remaining with old values too. Somehow Automapper doesn't change the property values which assigned before if you pass the destination instance as destionation parameter. I think, it is mostly relevant that Automapper was not designed for creating copy/clone objects.

Pin answered 15/12, 2017 at 15:41 Comment(1)
I can't change the object gi, must conserve it because it is pointed by UI, so if I create new object, it wont work for me the main program.Linskey

© 2022 - 2024 — McMap. All rights reserved.