Search code examples
c#asp.net-coreentity-framework-coreexpression-treesef-core-7.0

Why can't EF translate this Expression when it's derived from a virtual property?


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?


Solution

  • 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);
    }