Appsum Solutions
Appsum Solutions

Reputation: 1029

AutoMapper ProjectTo<>() not finding map

I have an ASP.NET 4.6.2 application. I wanted to use the ProjectTo<>() method of AutoMapper to project the results from the database to my viewmodels.

I've tried a lot of tests, but it seems that the map solely cannot be found when using the ProjectTo<>(). Using mapper.Map<>() on different locations with the same model and viewmodel perfectly works.

I guess there is something wrong with how AutoMapper works with my DI (Autofac), but I can't figure out what.

Startup.Cs

 public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            (...)

            // Autofac DI
            AutofacContainer = AutofacLoader.Configure(services).Build();

            return AutofacContainer.Resolve<IServiceProvider>();
        }

AutofacLoader.cs

public static ContainerBuilder Configure(IServiceCollection services)
        {
            var builder = new ContainerBuilder();

(...)


            // AutoMapper
            builder.RegisterModule<AutoMapperModule>();

            if (services != null)
            { 
                builder.Populate(services);
                
            }
            return builder;
        }

AutoMapperModule.cs

public class AutoMapperModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        var mapping = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new Core.Mappings.AutoMapperProfileConfiguration());
            cfg.AddProfile(new Dieet.Core.Mappings.AutoMapperProfileConfiguration());
        });
        builder.RegisterInstance(mapping.CreateMapper()).As<IMapper>().AutoActivate();
    }
}

The test that fails with 'Missing map from Patient to PatientViewModel. Create using Mapper.CreateMap<Patient, PatientViewModel>'.

   [Fact]
    public async void InfohosServiceReturnsPatientViewModels()
    {
        var db = _container.Resolve<IInfohosDb>();

        var search = new PaginatedSearchBase();
        search.OrderBy = "Naam";

        var mapper = _container.Resolve<IMapper>();

        var result = await search.PagedResultAsAsync<Patient,PatientViewModel >(null,db.Patienten,mapper);
    }

PaginatedSearchBase

public class PaginatedSearchBase
{
    public string OrderBy { get; set; }
    public bool OrderDescending { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

And finally the extension that calls the ProjectTo

public static class PagedResultExtensions
{
    public static async Task<PagedResult<T>> PagedResultAsync<T>(this PaginatedSearchBase vm, ICollection<Expression<Func<T, bool>>> whereCollection, IEnumerable<T> context) where T : class
    {
        int totalCount;
        var query = PrepareQuery(vm, whereCollection, context, out totalCount);
        
        return new PagedResult<T>
        {
            Results = await query.ToListAsync(),
            Page = vm.Page,
            PageSize = vm.PageSize,
            Total = totalCount
        };
    }
    public static async Task<PagedResult<TAs>> PagedResultAsAsync<T, TAs>(this PaginatedSearchBase vm, ICollection<Expression<Func<T, bool>>> whereCollection, IEnumerable<T> context, IMapper mapper) where T : class
    {
        int totalCount;
        var query = PrepareQuery(vm, whereCollection, context, out totalCount);

        return new PagedResult<TAs>
        {
----------> Results = await query.ProjectTo<TAs>(mapper).ToListAsync(),
            Page = vm.Page,
            PageSize = vm.PageSize,
            Total = totalCount
        };
    }

    private static IQueryable<T> PrepareQuery<T>(PaginatedSearchBase vm, ICollection<Expression<Func<T, bool>>> whereCollection, IEnumerable<T> context,
        out int totalCount) where T : class
    {
        var query = context.AsQueryable();
        if (whereCollection != null)
        {
            foreach (var w in whereCollection)
            {
                if (w != null)
                {
                    query = query.Where(w);
                }
            }
        }
        // Order by
        query = query.OrderBy($"{vm.OrderBy} {(vm.OrderDescending ? "DESC" : "ASC")}");

        // Total rows
        totalCount = query.Count();

        // Paging
        query = query.Skip((vm.Page - 1)*vm.PageSize).Take(vm.PageSize);
        return query;
    }
}

For information, I'm using versions:

A new test that I did to check if the mappings really work:

var mapper = _container.Resolve<IMapper>();
        var p = new Patient();
        p.Naam = "Test";
        var vm = mapper.Map<PatientViewModel>(p);

        vm.Naam.ShouldBeEquivalentTo("Test");

This test passes

When I use the Map<> in a Select() instead, it works too, so it's really the ProjectTo<>() that fails:

var results = await query.ToListAsync();
        return new PagedResult<TAs>
        {
            Results = results.Select(mapper.Map<TAs>).ToList(),
            Page = vm.Page,
            PageSize = vm.PageSize,
            Total = totalCount
        };

This works, but it requires the mapper to be included and not be injected and it doesn't use the ProjectTo that Automapper has for database access...

Upvotes: 7

Views: 15594

Answers (2)

Cubelaster
Cubelaster

Reputation: 342

You don't have to specifically add ConfigurationProvider to DI. If you already added the IMapper to DI, than you can read ConfigurationProvider from the Mapper itself. Example: I had the same problem and created a base class containing IMapper that got injected:

public abstract class ServiceBase
{
    public IMapper Mapper { get; set; }
}

This class was inherited in all my Services that used AutoMapper. Now every time any of my services needed to Map something, they did so:

    return context.SomeEntity
        .Where(e => e.Id == filter.Id)
        .ProjectTo<EntityDto>(Mapper.ConfigurationProvider).ToList();

With Mapper being injected. As long as you put the completely configured Mapper in DI, you're ok.

container.Register(Component.For<IMapper>().UsingFactoryMethod(x =>
    {
        return new AutoMapperConfig().ConfigureMapper();
    })
    );

Upvotes: 5

Jeff
Jeff

Reputation: 668

This is happening due to the recent move by Automapper away from having the entire API use static methods. Now that everything is instance-based, the static extension methods no longer know about the mapping configuration and they now have to be passed into the method.

I ended up registering an instance of the MapperConfiguration as IConfigurationProvider (I'm using Unity)

container.RegisterInstance(typeof (IConfigurationProvider), config);

This is injected into my query handler:

[Dependency]
public IConfigurationProvider MapperConfigurationProvider { get; set; }

Finally, the MapperConfigurationProvider is passed to the call to ProjectTo:

.ProjectTo<Payment>(MapperConfigurationProvider);

Upvotes: 13

Related Questions