Search code examples
automapperautomapper-3

Unsupported Mapping Exception - Missing type map configuration in Automapper?


I'm sure I am missing something simple. First, I'll show all the code I have written to wire up the plumbing, then I'll show the exception message. Then, I'll set out what I have tried to fix it.

LicenceTrackerProfile

public class LicenceTrackerProfile : Profile
{
    const string LicenceTrackerProfileName = "LicenceTrackerProfile";

    public override string ProfileName
    {
        get { return LicenceTrackerProfileName; }
    }

    protected override void Configure()
    {
        // initialize mappings here
        new ViewModelMappings(this).Initialize();
    }

}

MapperBootstrapper

public class MapperBootstrapper
{
    public void Configure()
    {
        var profile = new LicenceTrackerProfile();
        AutoMapper.Mapper.Initialize(p => p.AddProfile(profile));
    }

}

MappingBase

public abstract class MappingBase
{
    private readonly Profile _profile;

    protected MappingBase(Profile profile)
    {
        _profile = profile;
        _profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
        _profile.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
    }

    public Profile Profile
    {
        get { return _profile; }
    }
}

UniversalMapper

public class UniversalMapper : IUniversalMapper
{
    private readonly IMappingEngine _mappingEngine;

    public UniversalMapper(IMappingEngine mappingEngine)
    {
        _mappingEngine = mappingEngine;
    }

    public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
    {
        return _mappingEngine.Map(source, destination);
    }

}

ViewModelMappings

public class ViewModelMappings : MappingBase, IMappingInitializer
{
    private readonly Profile _profile;

    public ViewModelMappings(Profile profile) : base(profile)
    {
        _profile = profile;
        _profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
        _profile.DestinationMemberNamingConvention = new PascalCaseNamingConvention();

    }

    public void Initialize()
    {
        //  data to domain mappings
        Profile.CreateMap<EFDTO.Enums.FileTypes, Domain.FileTypes>();
        Profile.CreateMap<EFDTO.Licence, Domain.Licence>();
        Profile.CreateMap<EFDTO.LicenceAllocation, Domain.LicenceAllocation>();
        Profile.CreateMap<EFDTO.Person, Domain.Person>();
        Profile.CreateMap<EFDTO.Software, Domain.Software>();
        Profile.CreateMap<EFDTO.SoftwareFile, Domain.SoftwareFile>();
        Profile.CreateMap<EFDTO.SoftwareType, Domain.SoftwareType>();
    }
}

Note, the initialize method and Configure method are being called, so they're not being "missed".

Exception

Missing type map configuration or unsupported mapping.

Mapping types: Software -> Software LicenceTracker.Entities.Software -> LicenceTracker.DomainEntities.Software

Destination path: Software

Source value: LicenceTracker.Entities.Software

Troubleshooting Ignoring columns. I planned to ignore columns, starting with all and then eliminating them by un-ignoring them 1 by 1 until I found the problem columns. However, to my surprise, the error occurs when I ignore all columns:

Profile.CreateMap<EFDTO.Software, Domain.Software>()
    .ForMember(software => software.Licences, e => e.Ignore())
    .ForMember(software => software.Name, e => e.Ignore())
    .ForMember(software => software.SoftwareFiles, e => e.Ignore())
    .ForMember(software => software.Type, e => e.Ignore())
    .ForMember(software => software.Description, e => e.Ignore())
    .ForMember(software => software.Id, e => e.Ignore())
    .ForMember(software => software.TypeId, e => e.Ignore()
    .ForMember(software => software.ObjectState, e => e.Ignore());

The Domain entities have [DataContract] (at class level) and [DataMember] (at method level) attributes. I added each of those attributes to the EF entities as well.

Other than that, I am out of ideas. It all seems to be wired up correctly.

What did I miss?


Solution

  • I'm back to heroically answer my question.

    The problem was in the Service which created the UniversalMapper object (forgive the sloppy code, it is not final yet):

    public class LicenceTrackerService : ILicenceTrackerService, IDisposable
    {
        LicenceTrackerContext context = new LicenceTrackerContext();
        private MapperBootstrapper mapperBootstrapper;
        private IUniversalMapper mapper = new UniversalMapper(Mapper.Engine);
        private IUnitOfWork unitOfWork;
    
        public LicenceTrackerService()
        {            
            unitOfWork = new UnitOfWork(context, new RepositoryProvider(new RepositoryFactories()));
            mapperBootstrapper  = new MapperBootstrapper();
            mapperBootstrapper.Configure();
    
            Database.SetInitializer(new LicenceTrackerInitializer());
            context.Database.Initialize(true);
        }
    
        public int GetNumber()
        {
            return 42;
        }
    
        public List<LicenceTracker.DomainEntities.Software> GetSoftwareProducts()
        {
            var productsRepo = unitOfWork.Repository<Software>();
    
            var list = productsRepo.Query().Select().ToList();
    
            var softwareList = new List<LicenceTracker.DomainEntities.Software>();
    
            foreach (var software in list)
            {
                var softwareProduct = new LicenceTracker.DomainEntities.Software();
                softwareList.Add(Mapper.Map(software, softwareProduct));
            }
    
            return softwareList;
        }
    
        public void Dispose()
        {
            unitOfWork.Dispose();
        }
    }
    

    I'm still not sure why, but initializing the mapper outside of the constructor (default value style) was not happy. By moving that instantiation into the constructor of the service, it worked:

        private IUniversalMapper mapper;
    
        public LicenceTrackerService()
        {
            mapper = new UniversalMapper(Mapper.Engine);
            ...
        }
    

    There's obviously something about static properties (Mapper.Engine) and default instantiations that I'm not understanding.

    Anyway, no big deal as I was planning to inject the UniversalMapper into the service anyway.

    Edit I've actually figured out the problem for real now. It is an ordering thing. With Automapper, I had to initialize the mapper with the Profile before inserting the Mapper.Engine into the UniversalMapper.

    Obviously, the Get aspect of the Mapper.Engine property is not just a memory reference to an object. And yes, a quick glance at the code inside Automapper confirms that.

    So, assigning the result of the Get property to the _mappingEngine field of the UniversalMapper must happen after that engine has been configured.