Search code examples
c#inheritancepolymorphismautomapperautomapper-9

Map class with abstract property to destination


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.


Solution

  • 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));