You probably won’t need AutoMapper but…
AutoMapper nowadays is like caviar — you either like it or hate it with a passion.
Maybe you have seen it in the wild being used incorrectly by either:
- Doing unnecessary mappings;
- Hiding business logic;
- Overusing the library to the point that a change on a model will throw obscure mapping errors;
But regardless the love-hate feeling from our software engineer peers, the ProjectTo extension from AutoMapper can be pretty useful in certain cases:
The
.ProjectTo<T>()
will tell AutoMapper’s mapping engine to emit aselect
clause to the IQueryable that will inform entity framework that it only needs to query the Name column of a given table, same as if you manually projected yourIQueryable
to anT
with aSelect
clause.
Example
A method should return a list of humans that have at least one cat as a pet. Not every field from the domain entities but rather a set of them.
Our domain entities:
public class Human
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Prop1 { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public virtual ICollection<Pet> Pets { get; set; } = new HashSet<Pet>();
}
public class Pet
{
[Key]
public Guid Id { get; set; }
public string PetType { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public virtual Human Human { get; set; }
}
Our response models:
public class HumanResponse
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<PetResponse> Pets { get; set; } = new List<PetResponse>();
}
public class PetResponse
{
public Guid Id { get; set; }
public string PetType { get; set; }
}
Results
Without ProjectTo we can see that every field from both of our tables gets returned —which performance-wise is not that acceptable as we don’t really want to fetch everything.
public async Task<List<HumanResponse>> GetHumansWithoutProjection() {
var query = await _repositoryContext
.Humans
.Include(_ => _.Pets)
.Where(human => human.Pets.Any(pet => pet.PetType == PetType.Cat))
.Take(50)
.ToListAsync();
return _mapper.Map<List<HumanResponse>>(query);
}
With ProjectTo we can see that we are only fetching the data that our response is requiring.
public async Task<List<HumanResponse>> GetHumansWithProjection()
{
return await _repositoryContext
.Humans
.Where(human => human.Pets.Any(pet => pet.PetType == PetType.Cat))
.ProjectTo<HumanResponse>(_mapper.ConfigurationProvider)
.Take(50)
.ToListAsync();
}
Disclaimer
A Select could also be done instead of ProjectTo — probably it will be better on more complex data structures — but on dumb flattenings, AutoMapper can shine.
Benchmark
Although negligible in this context, it could be pretty significant in realistic scenarios with complex schemas.
You can see the actual project on github (Set up the SQL Server environment with Docker).
Happy coding and check out part 2 of this article!