AutoMapper LINQ Support Deep Dive
My favorite feature of AutoMapper is its LINQ support. If you're using AutoMapper, and not using its queryable extensions, you're missing out!
Normal AutoMapper usage is something like:
var dest = _mapper.Map<Dest>(source);
Which would be equivalent to:
var dest = new Dest {
Thing = source.Thing,
Thing2 = source.Thing2,
FooBarBaz = source.Foo?.Bar?.Baz
};
The main problem people run into here is that typically that source object is some object filled in from a data source, whether it's a relational or non-relational source. This implies that the original fetch pulled a lot more information back out than we needed to.
Enter projections!
Constraining data fetching with projection
If we wanted to fetch only the data we needed from the server, the quickest path to do so would be a projection at the SQL level:
SELECT
src.Thing,
src.Thing2,
bar.Baz as FooBarBaz
FROM Source src
LEFT OUTER JOIN Foo foo on src.FooId = foo.Id
LEFT OUTER JOIN Bar bar on foo.BarId = bar.Id
Well...that's already getting a bit ugly. Plus, all that joining information is already represented in our object model, why do we have to drop down to such a low level?
If we're using LINQ with our data provider, then we can use the query provider to perform a Select projection
var dest = await dbContext.Destinations
.Where(d => d.Id = id)
.Select(d => new Dest {
Thing = source.Thing,
Thing2 = source.Thing2,
FooBarBaz = source.Foo.Bar.Baz.
})
.FirstOrDefaultAsync();
And underneath the covers, the query provider parses the Select
expression tree and converts that to SQL, so that the SELECT
query against the database is only what we need and our query provider skips that data/domain model altogether to go from SQL straight to our destination type.
All is great! Except, of course, we're back to boring code, but this time, it's projections!
AutoMapper LINQ Support
Enter AutoMapper's LINQ support. Traditional AutoMapper usage is in in-memory objects, but several years ago, we also added the ability to automatically build out those Select
projections for you as well:
// Before
var dest = await dbContext.Destinations
.Where(d => d.Id = id)
.Select(d => new Dest { // Just automap this dumb junk
Thing = source.Thing,
Thing2 = source.Thing2,
FooBarBaz = source.Foo.Bar.Baz.
})
.FirstOrDefaultAsync();
// After
var dest = await dbContext.Destinations
.Where(d => d.Id = id)
.ProjectTo<Dest>() // oh so pretty
.FirstOrDefaultAsync();
These two statements are exactly equivalent. AutoMapper behind the scenes builds up the Select
projection expression tree exactly how you would do this yourself, except it uses the AutoMapper mapping configuration to do so.
We get the best of both worlds here - enforcement of our destination type conventions, got rid of all that dumb projection code, and safety with configuration validation.
But how does this all work?
Underneath the Covers
Behind the scenes, it all starts with extending out IQueryable
(not IEnumerable
) to create the ProjectTo
method:
public static class Extensions {
public static IQueryable<TDestination> ProjectTo<TDestination>(
this IQueryable source,
IConfigurationProvider configuration)
=> new ProjectionExpression(source, configuration.ExpressionBuilder)
.To<TDestination>();
}
I've pushed all the projection logic to a separate object, ProjectionExpression
. One critical thing we need to do is make sure we're returning the exact same IQueryable
instance at the end, so that you can do the extended behaviors that many ORMs support, such as async queries.
Next, we build up an expression tree that needs to be fed in to IQueryable.Select
:
public class ProjectionExpression {
// ctor etc
public IQueryable<TResult> To<TResult>() {
return (IQueryable<TResult>) _builder.GetMapExpression(
_source.ElementType,
typeof(TResult))
.Aggregate(_source, Select);
}
}
GetMapExpression
return IEnumerable<LambdaExpression>
, which we then use to reduce to the final Select
call on the IQueryable
instance:
private static IQueryable Select(IQueryable source, LambdaExpression lambda) => source.Provider.CreateQuery(
Expression.Call(
null,
QueryableSelectMethod.MakeGenericMethod(source.ElementType, lambda.ReturnType),
new[] { source.Expression, Expression.Quote(lambda) }
)
);
Calling in to the underlying IQueryProvider
instance ensures that we're using the query provider, instead of just plain Queryable
, to create the query. The underlying query provider often does "more" things that need to be tracked, and this also makes sure that the final IQueryable
is the one from the original query provider - not the built-in one in .NET.
The original GetMapExpression
method on the IExpressionBuilder
instance isn't too too exciting, we do some caching of building out expressions, and have a chain of responsibility pattern in place to decide how to bind each destination member based on different rules (things that need to be mapped, enumerables, strings etc.).
We start by finding the mapping configuration for the source/destination type pair, then for each destination member, pick an appropriate expression building strategy based on that the source/destination type pair for each member, including any member configuration you've put in place.
The end result is a Select
call to the correct IQueryProvider
instance, with a fully built-out expression tree, that the underlying IQueryProvider
instance can then take and build out the correct data projection straight from the server.
And because we simply chain off of IQueryProvider
, we can extend any query provider that also handles Select
.
When things go wrong
The expression trees we build up need to be parsed and understood by the underlying query provider. This is not always the case, and if you search EF or EF Core for issues containing the words "AutoMapper", you'll find many, many issues with that expression parsing/translation (> 100 in the EF Core repository alone). It's not a perfect system, and expression tree parsing is a hard problem.
When you run into an issue with the expression tree, you'll get some weird wonky error message from the query provider. When you do, the easiest thing to diagnose is to drop back down in to "raw" LINQ, calling Select
manually. Once you do, you can remove members one-by-one until you find the underlying problem mapping, and join dozens of others opening issues in the appropriate GitHub repository :).
For nearly all cases of straightforward projection, it works great! So if you're using AutoMapper, check out ProjectTo
, as it's the greatest (mapping) thing since sliced bread.