I want to define an Expression<Func<TSource,TKey>> KeyExpr
such as you would pass to IQueryable.OrderBy
, and then combine it with a TKey
value to derive an Expression<Func<TSource,bool>> WithKeyExpr
such as you would pass to IQueryable.Single
. I learned how to do this in another Q&A and confirmed that the accepted answer works.
The problem I'm encountering now is that EF cannot translate WithKeyExpr
when KeyExpr
is virtual. As a baseline, I wrote a controller that defines these expressions non-virtually:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private ErpContext Db { get; }
private Expression<Func<Country, int>> KeyExpr { get; } = e => e.CountryId;
private Expression<Func<Country, bool>> WithKeyExpr(int key) =>
Expression.Lambda<Func<Country, bool>>(Expression.Equal(KeyExpr.Body, Expression.Constant(key)), KeyExpr.Parameters);
public TestController(ErpContext db)
{
Db = db;
}
[HttpGet]
[Route("[action]")]
[ProducesResponseType(typeof(IEnumerable<Country>), StatusCodes.Status200OK)]
public async Task<IEnumerable<Country>> GetAsync()
{
return await Db.Countries.OrderBy(KeyExpr).ToListAsync();
}
[HttpGet]
[Route("{id}/[action]")]
[ProducesResponseType(typeof(Country), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetAsync(int id)
{
var country = await Db.Countries.SingleOrDefaultAsync(WithKeyExpr(id));
return country == null ? NotFound() : Ok(country);
}
}
Both of these controller actions work perfectly.
To reproduce the error I'm getting, I made KeyExpr
abstract. Everything else, including WithKeyExpr
, is defined in exactly the same way as before:
[ApiController]
[Route("[controller]")]
public abstract class TestControllerBase : ControllerBase
{
private ErpContext Db { get; }
protected abstract Expression<Func<Country, int>> KeyExpr { get; }
private Expression<Func<Country, bool>> WithKeyExpr(int key) =>
Expression.Lambda<Func<Country, bool>>(Expression.Equal(KeyExpr.Body, Expression.Constant(key)), KeyExpr.Parameters);
public TestControllerBase(ErpContext db)
{
Db = db;
}
[HttpGet]
[Route("[action]")]
[ProducesResponseType(typeof(IEnumerable<Country>), StatusCodes.Status200OK)]
public async Task<IEnumerable<Country>> GetAsync()
{
return await Db.Countries.OrderBy(KeyExpr).ToListAsync();
}
[HttpGet]
[ProducesResponseType(typeof(Country), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Route("{id}/[action]")]
public async Task<IActionResult> GetAsync(int id)
{
var country = await Db.Countries.SingleOrDefaultAsync(WithKeyExpr(id));
return country == null ? NotFound() : Ok(country);
}
}
Then I derived a concrete controller:
public class TestController : TestControllerBase
{
public TestController(ErpContext db) : base(db) { }
protected override Expression<Func<Country, int>> KeyExpr => o => o.CountryId;
}
Get()
still works, demonstrating that EF can translate the virtual KeyExpr
. But Get(int)
does not work; EF cannot translate the WithKeyExpr
derived from the virtual KeyExpr
. The error is:
The LINQ expression 'o' could not be translated.
Why can EF no longer translate WithKeyExpr
?
The problem is here:
protected override Expression<Func<Country, int>> KeyExpr => o => o.CountryId;
This will return a new expression instance for each call to the KeyExpr
property, so o
in the KeyExpr.Parameters
and o
used in the KeyExpr.Body
will be different instances of ParameterExpression
(with the same parameter name) which leads to the error in the question. One way to fix it is to change the child class to:
public class TestController : TestControllerBase
{
// ...
protected override Expression<Func<Country, int>> KeyExpr { get; } = o => o.CountryId;
}
Or change WithKeyExpr
to:
private Expression<Func<Country, bool>> WithKeyExpr(int key)
{
var keyExpr = KeyExpr;
return Expression.Lambda<Func<Country, bool>>(Expression.Equal(keyExpr.Body, Expression.Constant(key)),
keyExpr.Parameters);
}