1

I'm having some issues mapping a container class that contains an abstract property to my View Model destination class.

Mapping Source Classes

//Container class
public class GiftcardDetailResponse : Response
{
    //Instance of either UserGiftcardDTO or ImportedGiftcardDTO
    public GiftcardInstanceDTO UserGiftcard { get; set; }
}

public abstract class GiftcardInstanceDTO : BaseDTO
{
    public int UserId { get; set; }   
    public decimal Balance { get; set; } 
    public string BarcodeValue { get; set; }
    public string BarcodeUrl { get; set; }
    public string Code { get; set; }
    public string Pin { get; set; }
    public bool RefreshBalanceSupported { get; set; }
    public bool Viewed { get; set; }
    public bool IsArchived { get; set; }
    public virtual UserDTO User { get; set; }
}

public class UserGiftcardDTO : GiftcardInstanceDTO
{
    public int GiftcardId { get; set; }
    public DateTimeOffset? ActivatedAt { get; set; }
    public DateTimeOffset? BalanceUpdatedAt { get; set; }
    public string ClaimUrl { get; set; }
    public string ClaimSecret { get; set; }
    public PrivacySettings Privacy { get; set; }
    public bool IsPending { get; set; }
    public bool BoughtAsGift { get; set; }
    public virtual GiftcardDTO Giftcard { get; set; }
}

public class ImportedGiftcardDTO : GiftcardInstanceDTO
{
    public string RetailerName { get; set; }
    public string FrontImage { get; set; }
    public string BackImage { get; set; }
}

Mapping Destination Classes

//Front-end view model
public class GiftcardDetailViewModel
{
    public int Id { get; set; }
    public string RetailerName { get; set; }
    public decimal Amount { get; set; }
    public string Image { get; set; }
}

Mapping Configuration

        CreateMap<GiftcardDetailResponse, GiftcardDetailViewModel>()
            .IncludeMembers(src => src.UserGiftcard);

        //Automapper should pick a concrete mapping for this
        CreateMap<GiftcardInstanceDTO, GiftcardDetailViewModel>()
            .IncludeAllDerived();

        //Concrete mappings to View Model
        CreateMap<UserGiftcardDTO, GiftcardDetailViewModel>()
            .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dest => dest.Image, opt => opt.MapFrom(src => src.Giftcard.Image))
            .ForMember(dest => dest.Amount, opt => opt.MapFrom(src => src.Balance))
            .ForMember(dest => dest.RetailerName, opt => opt.MapFrom(src => src.Giftcard.Merchant.Name));

        CreateMap<ImportedGiftcardDTO, GiftcardDetailViewModel>()
            .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dest => dest.Image, opt => opt.MapFrom(src => src.FrontImage))
            .ForMember(dest => dest.Amount, opt => opt.MapFrom(src => src.Balance))
            .ForMember(dest => dest.RetailerName, opt => opt.MapFrom(src => src.RetailerName));

The problem is that Automapper isn't picking my explicit mappings for the derived class of my abstract property when I map GiftcardDetailResponse to GiftcardDetailViewModel. For example, if my code looked this

        var containerClass = new GiftcardDetailResponse();
        containerClass.UserGiftcard = new ImportedGiftcardDTO();

        var viewModel = MapperWrapper.Mapper.Map<GiftcardDetailViewModel>(containerClass);

The execution tree looks like this

//Automapper generated execution plan
(src, dest, ctxt) =>
{
    GiftcardDetailViewModel typeMapDestination;
    return (src == null)
    ? null
    : {
        typeMapDestination = dest ?? new GiftcardDetailViewModel();
        try
        {
            var resolvedValue = ((src == null) || ((src.UserGiftcard == null) || false)) ? default(int) : src.UserGiftcard.Id;
            typeMapDestination.Id = resolvedValue;
        }
        catch (Exception ex)
        {
            throw new AutoMapperMappingException(
                "Error mapping types.",
                ex,
                AutoMapper.TypePair,
                TypeMap,
                PropertyMap);

            return default(int);
        }

        return typeMapDestination;
    };
}

It only seems to pick only the properties that share the same name in both GiftcardInstanceDTO and GiftcardDetailViewModel instead of using my defined mappings.

But what I'm looking for would be something similar to the execution tree when I only explicitly map my abstract property to my view model such as

var propertyModel = MapperWrapper.Mapper.Map<GiftcardDetailViewModel>(containerClass.UserGiftcard);

which correctly shows me my derived mapping

//Automapper generated execution plan
(src, dest, ctxt) =>
{
GiftcardDetailViewModel typeMapDestination;
return (src == null)
    ? null
    : {
        typeMapDestination = dest ?? new GiftcardDetailViewModel();
        try
        {
            var resolvedValue =
            {
                try
                {
                    return ((src == null) || false) ? default(int) : src.Id;
                }
                catch (NullReferenceException)
                {
                    return default(int);
                }
                catch (ArgumentNullException)
                {
                    return default(int);
                }
            };

            typeMapDestination.Id = resolvedValue;
        }
        catch (Exception ex)
        {
            throw new AutoMapperMappingException(
                "Error mapping types.",
                ex,
                AutoMapper.TypePair,
                TypeMap,
                PropertyMap);

            return default(int);
        }
        try
        {
            var resolvedValue =
            {
                try
                {
                    return ((src == null) || false) ? null : src.RetailerName;
                }
                catch (NullReferenceException)
                {
                    return null;
                }
                catch (ArgumentNullException)
                {
                    return null;
                }
            };

            var propertyValue = (resolvedValue == null) ? null : resolvedValue;
            typeMapDestination.RetailerName = propertyValue;
        }
        catch (Exception ex)
        {
            throw new AutoMapperMappingException(
                "Error mapping types.",
                ex,
                AutoMapper.TypePair,
                TypeMap,
                PropertyMap);

            return null;
        }
        try
        {
            var resolvedValue =
            {
                try
                {
                    return ((src == null) || false) ? null : src.FrontImage;
                }
                catch (NullReferenceException)
                {
                    return null;
                }
                catch (ArgumentNullException)
                {
                    return null;
                }
            };

            var propertyValue = (resolvedValue == null) ? null : resolvedValue;
            typeMapDestination.Image = propertyValue;
        }
        catch (Exception ex)
        {
            throw new AutoMapperMappingException(
                "Error mapping types.",
                ex,
                AutoMapper.TypePair,
                TypeMap,
                PropertyMap);

            return null;
        }
        try
        {
            var resolvedValue =
            {
                try
                {
                    return ((src == null) || false) ? default(decimal) : src.Balance;
                }
                catch (NullReferenceException)
                {
                    return default(decimal);
                }
                catch (ArgumentNullException)
                {
                    return default(decimal);
                }
            };

            typeMapDestination.Amount = resolvedValue;
        }
        catch (Exception ex)
        {
            throw new AutoMapperMappingException(
                "Error mapping types.",
                ex,
                AutoMapper.TypePair,
                TypeMap,
                PropertyMap);

            return default(decimal);
        }

        return typeMapDestination;
    };
}

The documentation says I can use IncludeMembers to flatten child objects to the destination object when theres already defined mappings. But this behavior doesn't seem to be working correctly in this instance when the child object is abstract.

Dillon Drobena
  • 761
  • 1
  • 8
  • 26
  • `IncludeMembers` is not designed to work that way. You can use `AfterMap` to call `Map` and pass the destination object. – Lucian Bargaoanu Aug 07 '20 at 09:31
  • Is it just because my child member is abstract? Because the example shown in the documentation is very similar https://docs.automapper.org/en/stable/Flattening.html#includemembers – Dillon Drobena Aug 07 '20 at 13:13
  • 1
    No, it's because the map for the child member is not resolved at runtime, is resolved beforehand, at config time, it's static. `Include` works by resolving the map at runtime, when you call `Map`. – Lucian Bargaoanu Aug 07 '20 at 13:35
  • So far adding AfterMap doesn't seem to be working. Just returns a blank object if I try to overwrite the destination with my derived mapping. I'll try adding a custom resolver and see if that works – Dillon Drobena Aug 07 '20 at 15:03
  • 1
    `AfterMap((source, destination, context) => context.Map(source.UserGiftcard, destination)` – Lucian Bargaoanu Aug 07 '20 at 15:06
  • Yay that worked! I was trying to do ```AfterMap((src, dst) => dst = MapperWrapper.Mapper.Map(src.UserGiftcard, dst)``` – Dillon Drobena Aug 07 '20 at 15:19
  • Feel free to add that as an answer and I'll accept it – Dillon Drobena Aug 07 '20 at 15:20

1 Answers1

1

IncludeMembers does not implement this "dynamic" behavior, but you can do smth like this:

 CreateMap<Source, Destination>().AfterMap((source, destination, context) => context.Mapper.Map(source.InnerSource, destination));
Lucian Bargaoanu
  • 3,336
  • 3
  • 14
  • 19
  • I was about to file a GitHub issues because of this. The comment about being resolved at config time vs runtime, static vs dynamic, was great to understand what's going on. Would be great to have a high-level view of how Automapper works in the docs, to avoid this type of pitfall. – hmijail Sep 08 '21 at 06:41
  • 1
    https://github.com/AutoMapper/AutoMapper/commit/6e040cca406813251aef05b4faf8c24b67de0626 – Lucian Bargaoanu Sep 08 '21 at 07:28