fabspro fabspro - 4 months ago 45
C# Question

Get combined Expression<TDelegate> from IQueryable

Following on from a previous question (Dynamically generate lambda expression with constants from variables), my goal is to apply a workaround to a large number of complex LINQ EF queries in existing code.

As an example, queries are built up as follows:

var query = Entities.Users.Where(u => u.UserName == varUserName);
query = query.Where(u => u.IsUserLocked == varUserLocked);
query = query.Where(u => u.LastModifiedAt > varLastModifiedAt);


And then results are derived as:

var results = query.ToList();


To work around an issue in the EF provider I am using, I have had to modify the Where() expressions as follows:

var query = Entities.Users.Where(OracleEFQueryUtils.ReplaceVariablesWithConstants<Func<Entity.User, bool>>(u => u.UserName == varUserName));
query = query.Where(OracleEFQueryUtils.ReplaceVariablesWithConstants<Func<Entity.User, bool>>(u => u.IsUserLocked == varUserLocked));
query = query.Where(OracleEFQueryUtils.ReplaceVariablesWithConstants<Func<Entity.User, bool>>(u => u.LastModifiedAt > varLastModifiedAt));


As can be seen, while this works, it results in very verbose code, and it also means that a large existing codebase must be migrated to use this new method.

I am searching for an easy way to apply this method to all Where() expressions.

So far I have thought of two approaches which I am researching:


  • Approach 1: Inherit from System.Linq.Queryable and override Where() to add the necessary method call. Then change all affected files to be using my own class at the top of the file, instead of using System.Linq.

  • Approach 2: Create a new method that takes an IQueryable, and processes the expression tree to return a new IQueryable that has had all variables replaced with constants, similar to the effect of manually calling my method on all Where() expressions.



I am preferring approach 2 because it intuitively feels cleaner, however I run into the problem of not being able to find the combined expression tree to amend. I have found the IQueryable.Expression property, but I cannot find how to proceed from there.

For reference, my implementation of OracleEFQueryUtils is as follows:

class OracleEFQueryUtils
{
public static Expression<TDelegate> ReplaceVariablesWithConstants<TDelegate>(Expression<TDelegate> source)
{
return source.Update(
new ReplaceVariablesWithConstantsVisitor().Visit(source.Body),
source.Parameters);
}

class ReplaceVariablesWithConstantsVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
var expression = Visit(node.Expression);
if (expression is ConstantExpression)
{
var variable = ((ConstantExpression)expression).Value;
var value = node.Member is FieldInfo ?
((FieldInfo)node.Member).GetValue(variable) :
((PropertyInfo)node.Member).GetValue(variable);
return Expression.Constant(value, node.Type);
}
return node.Update(expression);
}
}
}


I may be attempting to do something that is unsupported by the framework, however I look forward to any ideas! Thanks.

Answer

I'd look into creating extensions to override the methods for the query operations you want to support. Depending on how far you want to go with this, it can produce some nice looking code.

public interface IOracleQueryable<T> : IQueryable<T> { }

public static class OracleQueryExtensions
{
    public static IOracleQueryable<TSource> AsOracleQueryable<TSource>(
            this IQueryable<TSource> source) => new OracleQueryable<TSource>(source);

    public static IOracleQueryable<TSource> Where<TSource>(
            this IOracleQueryable<TSource> source, Expression<Func<TSource, bool>> predicate) =>
        Queryable.Where(source, Replace(predicate)).AsOracleQueryable();

    private static Expression<TDelegate> Replace<TDelegate>(Expression<TDelegate> expr) =>
        OracleEFQueryUtils.ReplaceVariablesWithConstants<TDelegate>(expr);

    private class OracleQueryable<TSource> : IOracleQueryable<TSource>
    {
        public OracleQueryable(IQueryable<TSource> source) { Source = source; }
        private IQueryable<TSource> Source { get; }

        public Expression Expression => Source.Expression;
        public IQueryProvider Provider => Source.Provider;
        public Type ElementType => Source.ElementType;
        public IEnumerator<TSource> GetEnumerator() => Source.GetEnumerator();
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
            GetEnumerator();
    }
}

Then all you need to do now is to remember to add AsOracleQueryable() to your queries.

var query = Entities.Users.AsOracleQueryable()
    .Where(u => u.UserName == varUserName)
    .Where(u => u.IsUserLocked == varUserLocked)
    .Where(u => u.LastModifiedAt > varLastModifiedAt);

It should even work fine in the query syntax.

var query =
    from u in Entities.Users.AsOracleQueryable()
    where u.UserName == varUserName
    where u.IsUserLocked == varUserLocked
    where u.LastModifiedAt > varLastModifiedAt
    select u;