Search code examples
c#entity-frameworkentity-framework-6automapperautomapper-4

Automapper 4.2.1 LINQ projections work only with static Mapper.CreateMap?


I'm trying to use Automapper projections on Entity Framework IQueryables.

On application start, I create and add all my mapping profiles which create maps with the non-static CreateMap method.

All those profiles are registered within my IoC container.

I get the missing mapping exception although I see the mapping profile in the instance of my mappingConfiguration.

What could be the problem? Am I missing something? I'm using Automapper 4.2.1

I've noticed that when adding a static Mapper.CreateMap, it works fine. Do projections work only with static API? I want to avoid the static API.

Full code:

public class ItemEntityToItemView : Profile
{
    public override void Configure()
    {
        CreateMap<ItemEntity, ItemView>();

        // Without this line, I get missing Map type configuration.
        Mapper.CreateMap<ItemEntity, ItemView>();
    }
}


public interface IEntitiesProjector
{
    IQueryable<T> SelectTo<T>(IQueryable source);
}

public class EntitiesProjector : IEntitiesProjector
{
    private readonly IMapperConfiguration _mapperConfig;

    public EntitiesProject(IMapperConfiguration mapperConfig)
    {
        _mapperConfig = mapperConfig;
    }

    public IQueryable<T> SelectTo<T>(IQueryable source)
    {
        return source.ProjectTo<T>(_mapperConfig);
    }
}

public class ItemsRepository : IITemsRepository
{
    public IQueryable<ItemEntity> GetById(int id)
    {
        return _dbSet.Where(x => x.Id == id);
    }
}

public class Service
{
    private readonly IEntitiesProjector _projector;

    public Service(IEntitiesProject entitiesProjector)
    {
        _projector = entitiesProjector;
    }

    public List<T> GetItem(int id)
    {
        IQueryable<ItemEntity> itemsQueryable = ItemsRepository.GetById(id);

        return _projector.SelectTo<ItemView>(itemsQueryable);
    }
}

My Autofac registration :

builder.RegisterAssemblyTypes().AssignableTo(typeof(Profile)).As<Profile>();

builder.Register(c => new MapperConfiguration(cfg =>
{
    cfg.CreateMap<IdentityUser, AspNetUser>().ReverseMap();
})).AsSelf().As<IMapperConfiguration>().SingleInstance();

builder.Register(c => c.Resolve<MapperConfiguration>().CreateMapper(c.Resolve)).As<IMapper>().InstancePerLifetimeScope();

builder.Register<EntitiesProjector>().As<IEntitiesProjector>().SingleInstance();

Solution

  • The reason is the following block:

    public class EntitiesProjector : IEntitiesProjector
    {
        private readonly IMapperConfiguration _mapperConfig;
    
        public EntitiesProject(IMapperConfiguration mapperConfig)
        {
            _mapperConfig = mapperConfig;
        }
    
        public IQueryable<T> SelectTo<T>(IQueryable source)
        {
            return source.ProjectTo<T>(_mapperConfig);
        }
    }
    

    source.ProjectTo is an extension method which has 5 overloads. In documentation they are passing instance of MappingConfiguration class there, and you are passing instance of IMapperConfiguration (interface). You think it will have the same effect, but it does not. IMapperConfiguration interface does not implement IConfigurationProvider interface, and that (IConfigurationProvider) is what correct overload of ProjectTo accepts. But, there is another overload of ProjectTo, which accepts "object parameters". Because it accepts object - it will match anything which did not fit other overloads. So what you are really calling is ProjectTo(object) overload, which has nothing to do with configuration, and your IMapperConfiguration together with profiles and maps is completely ignored.

    Quickfix will be

    public class EntitiesProjector : IEntitiesProjector
    {
        private readonly IConfigurationProvider _mapperConfig;
    
        public EntitiesProjector(IMapperConfiguration mapperConfig)
        {
            _mapperConfig = (IConfigurationProvider)mapperConfig;
        }
    
        public IQueryable<T> SelectTo<T>(IQueryable source)
        {
            return source.ProjectTo<T>(_mapperConfig);
        }
    }
    

    But of course you should better register your configuration as IConfigurationProvider in your container, that is just quick fix to ensure problem is really here.

    As for static Mapper.CreateMap - well it's static, so works regardless of what you pass to ProjectTo.

    As a side note this shows you how to not design api. Whenever you have many overloads and one of them accepts generic object and does completely different thing than all other overloads - that is asking for trouble.