No Support for Tuples in Expression Trees
Tuples, as added to C# 7, can be a nice alternative to anonymous types in LINQ when you only want to return a subset of values from the queried type.
Before tuples, this was only possible by creating an anonymous type in the Select
method:
var query = persons.Select(person => new { person.Name, person.Surname });
Now you can create a tuple instead:
var persons = new List<Person>();
var query = persons.Select(person => (person.Name, person.Surname));
However, if you try to do that with EF Core, the code won't compile:
using (var context = new PersonContext())
{
var query = context.Persons.Select(person => (person.Name, person.Surname));
}
The compiler will emit the following error:
An expression tree may not contain a tuple literal.
How come that it works just fine when querying a local collection but not when using EF Core? The reason is in different signatures of the Select
method. When used with a local collection, an IEnumerable
extension method is used:
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector);
With EF Core, a matching IQueryable
extension method is used instead:
public static IQueryable<TResult> Select<TSource, TResult>(
this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector);
Notice that the type of the second parameter is different:
- The
IEnumerable
version simply accepts the provided delegate. - The
IQueryable
version accepts an expression tree of that delegate type instead.
EF Core (like other LINQ providers) requires the expression tree because it needs to analyze it to create a SQL query matching the provided lambda.
Let's look at the following working query:
using (var context = new PersonContext())
{
var query = context.Persons.Select(person => new { person.Name, person.Surname });
}
The expression tree passed to the LINQ provider would match the one created with the following code:
var anonymousType = new { Name = "", Surname = "" }.GetType();
var parameter = Expression.Parameter(typeof(Person), "person");
var expression = Expression.Lambda(
Expression.New(
anonymousType.GetConstructor(new[] { typeof(string), typeof(string) }),
Expression.Property(parameter, "Name"),
Expression.Property(parameter, "Surname")),
parameter);
Fortunately, the compiler does the work for us and automatically compiles the lambda into a matching expression tree.
When trying to use a tuple instead of the anonymous type, the code doesn't compile because the expression trees API wasn't expanded with support for tuples when these were added to the language. There aren't any nodes which would describe the tuples and operations involving them.
That's unfortunate because tuples have a useful advantage over anonymous types - they can be a part of a method signature. Without tuples one would need to create a custom class with the required properties if that type needed to be returned from a method:
public static PersonName GetPersonName(this PersonContext context, int id)
{
return context.Persons
.Where(person => person.Id == id)
.Select(person => new PersonName(person.Name, person.Surname))
.First();
}
With tuples, that's not necessary anymore. A method can return a tuple. But because lambdas involving tuples can't be passed to a LINQ provider, the above code can't simply be rewritten to use a tuple instead of the custom class.
There's a workaround though. The query could still be written using an anonymous class. The returned value could then be converted locally into a tuple:
public static (string name, string surname) GetPersonName(this PersonContext ctx, int id)
{
return ctx.Persons
.Where(person => person.Id == id)
.Select(person => new { person.Name, person.Surname })
.ToList()
.Select(person => (person.Name, person.Surname))
.First();
}
This works because the ToList
method call executes the query and creates a local collection with the query results. As we have learned before, a lambda involving a tuple will work with a local collection. Such implementation has performance implications but depending on the case it might still be appropriate.