I have a ASP.NET 5 (running on 4.6.2, not Core) application. I wanted to use the ProjectTo<>() method of AutoMapper to project the results from the database to my viewmodels.
I've tried alot 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.
Anyway, the code:
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'.
[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:
Edit:
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
Edit 2:
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...
I ran into the same issue but managed to get it working. 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);
Hope this helps.
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();
})
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With