Projecting Named Tuples In Entity Framework Core
[Entity Framework Core, C#, .NET]
Suppose you had the following class:
public class Bank
{
public Guid BankID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
And suppose you were persisting it using Entity Framework 7.
You query the database like this:
public async Task<List<Bank>> GetList(CancellationToken cancellationToken)
{
return await _context.Banks.ToListAsync(cancellationToken);
}
No suppose you wanted to get back the data as a collection of named tuples.
You would write a method like this:
public List<(Guid BankID, string Code)> GetDetails()
{
return _context.Banks.Select(x => (x.BankID, x.Code)).ToList();
}
This, however, will not compile; and you will get the following error:
An expression tree cannot contain a tuple literal
The problem here is that Entity Framework does not support projection to named tuples, or, for that matter, any kind of tuples.
Which is surprising, because the code works perfectly for a normal collection.
var banks = new List<Bank>();
banks.AddRange(new[] {
new Bank { BankID = Guid.NewGuid(), Code = "1", Name = "One" },
new Bank { BankID = Guid.NewGuid(), Code = "2", Name = "Two" }
});
banks.Select(b => (b.BankID, b.Code));
Why does this work but not the EF version?
Because the collection version works against IEnumerable, but the Entity Framework version works against IQueryable
However there is a way around this:
- Project the properties you would like to return into an anonymous type
- Retrieve the collection of anonymous types
- Perform the projection on the collection
Like so:
public List<(Guid BankID, string Code)> GetDetails()
{
return _context.Banks.Select(x => new { x.BankID, x.Code })
.AsEnumerable()
.Select(x => (x.BankID, x.Code))
.ToList();
}
The reason this works is after the AsEnumerable call, we are no dealing with an IEmunerable instead of an IQueryable.
If you want to filter the returned data in some way, put the filter condition BEFOFE the .AsEnumerable() so that you don’t unnecessarily load all the data only to filter and throw away what you don’t need later.
Like so:
return _context.Banks.Where(x=>x.Code.StartsWith("A"))
.Select(x => new { x.BankID, x.Code })
.AsEnumerable()
.Select(x => (x.BankID, x.Code))
.ToList();
Happy hacking!