1

Trying to write an ExpressionVisitor that would rewrite a query

Starting point

var contractSubjectKey = new ContractSubjectKey(subjectId, rank);

insurance = context.Set<Insurance>()
    .FirstOrDefault(i => i.Key.ContractSubjectKey == contractSubjectKey);

After rewrite expected

i => i.Key.ContractSubjectKey.Id == contractSubjectKey.Id

I'm trying to rewrite BinaryExpression here so it goes and compares properties itself. The left side of operation I think i got covered. Initial left side of BinaryExpression is MemberExpression.

var left = Expression.Property(memberExpression, "Id");

So I just access property itself.

In case i use for right side ConstantExpression

var right = Expression.Constant(1, typeof(int));

so the resulting operation looks like

i.Key.ContractSubjectKey.Id == 1

it gets translated to SQL no problem, and retrieves data.

I do have problem with the right side. Right side is ParameterExpression. Parameter values I cannot find anywhere in initial Expression or QueryExpressionEventData (working with IQueryExpressionInterceptor). Not sure I understand it fully. It represents a local variable, evaluates during translation, something close to constant?
Anyway, if i try PropertyExpression

var right = Expression.Property(parameterExpression, "Id");

it seems i get proper looking expression, but during translation it fails `The Linq expression .... could not be translated

System.InvalidOperationException : The LINQ expression 'DbSet() .Where(i => EF.Property(EF.Property(i, "Key"), "ContractSubjectKey").Id == __contractSubjectKey_0.Id)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.

So problem is somewhere in evaluation value for Id on local variable?

Tried many things... not gonna waste space here.

One additional piece of context

public record ContractSubjectKey(int Id, int Rank);

//insurance.Key.ContractSubjectKey is actually ContractSubjectKeyInsurance which inherits from ContractSubjectKey
public record ContractSubjectKeyInsurance : ContractSubjectKey
{
    public ContractSubjectKeyInsurance(int Id, int Rank) : base(Id, Rank)
    {

    }
    public ContractSubjectKeyInsurance(int Id, int Rank, int InsuranceKeyId) : base(Id, Rank)
    {
        _insuranceKeyId = InsuranceKeyId;
    }

    private int _insuranceKeyId;
}

public record InsuranceKey(int Id, ContractSubjectKey ContractSubjectKey)    
{
    this.Id = Id;
    _contractSubjectId = ContractSubjectKey.Id;
    _contractSubjectRank = ContractSubjectKey.Rank;
    this.ContractSubjectKey = 
       new ContractSubjectKeyInsurance(ContractSubjectKey.Id,  ContractSubjectKey.Rank, Id);
}

EDIT

So, the issue is not the expression rewrite itself. I'm working with EF Core. I want to plug in IQueryExpressionInterceptor, that would do such expression change for every required query. The issue here is, there is IQueryTranslationPreprocessor, which extracts and caches the parameters. So the starting query is member access / constant expression, and you can rewrite it with just additional property access, once the query reaches IQueryExpressionInterceptor it is no longer possible. Link below to ticket that i think can describe the situation.
https://github.com/dotnet/efcore/issues/30208

Possible solution include custom IQueryTranslationPreprocessor/Postprocessor. Postprocessor should be able to access the parameter value. Preprocessor you can adjust initial query or caching of parameters. Developers of EF Core warn about adjusting Preprocessor as this can cause performance pitfalls.

1 Answer 1

0

So you're starting with the caller passing in an expression like;

Expression<Func<T,bool>> lambda = i => i.Key.ContractSubjectKey == contractSubjectKey;

To capture variables, the C# compiler translates the right hand side into something equivalent to;

public struct Captures{
    ContractSubjectKey contractSubjectKey;
}
var rhs = Expression.Field(
    Expression.Constant(
        new Captures{
            contractSubjectKey = contractSubjectKey
        },
        typeof(Captures)),
    nameof(contractSubjectKey));

The simplest you could do is just replace the BinaryExpression with an extra property access;

    protected override Expression VisitBinary(BinaryExpression node)
    {
        var left = Visit(node.Left);
        var right = Visit(node.Right);

        if (left.Type == typeof(ContractSubjectKey) 
            && right.Type == typeof(ContractSubjectKey) 
            && node.NodeType == ExpressionType.Equal)
            return Expression.Equal(
                Expression.Property(left, "Id"), 
                Expression.Property(right, "Id"));

        return node.Update(
            left,
            VisitAndConvert(node.Conversion, nameof(VisitBinary)),
            right);
    }

That way EF Core will still bind the captured variable to an sql parameter.

If you really wanted to replace the sql parameter with a constant, you could also visit any member access where the lhs is a constant. Then you can use reflection to access the value and return that as a constant instead.

    protected override Expression VisitMember(MemberExpression node)
    {
        var expr = Visit(node.Expression);
        if (expr is ConstantExpression c)
        {
            var obj = c.Value;
            if (node.Member is PropertyInfo p)
                obj = p.GetValue(obj);
            else if (node.Member is FieldInfo f)
                obj = f.GetValue(obj);
            return Expression.Constant(obj, node.Type);
        }
        return node.Update(expr);
    }

But I wouldn't recommend doing that. Using an sql parameter is vastly more efficient for both EF Core and your database.

3
  • I tried that, the property access. It is unable to translate. contractSubjectKey is captured/in closure, but is not part of the LambdaExpression.Parameters, it is like just a placeholder, value is pulled out of some dictionary later (based on errors i saw when trying renaming), and value is just plugged in after this interceptor. If I add property access, it is unable to translate. And it is ParameterExpression, no member access, no constant expression.
    – Matus
    Commented Jul 10 at 9:08
  • It seems that initial query Expression<Func<Insurance, bool>> query = i => i.Key.ContractSubjectKey == contractSubjectKey; does store conractSubjectKey as member access, and ConstantExpression below it, but by the time it gets to my implementation of IQueryExpressionVisitor it gets changed, contractSubjectKey is changed do ParameterExpression
    – Matus
    Commented Jul 10 at 9:42
  • Hmmm, so the query compiler extracts sql parameters first. Probably as part of testing if the shape of the query is the same, so the result of the compiler can be cached. That might make this change impossible. I haven't explored exactly how the query changes before EF Core hooks. Commented Jul 11 at 1:04

Not the answer you're looking for? Browse other questions tagged or ask your own question.