Search code examples
c#linq-expressionsspecification-pattern

Reusable expression


Given a query with a Where clause

CollectionA.Where(a => a.Prop1 == val1 && a.Prop2 == val2)

and another query with a similar Where clause but the properties are linked via the Reference.

CollectionB.Where(b => b.Reference.Prop1 == val1 && b.Reference.Prop2 == val2)

For functions this does work:

        Func<A, bool> f1 = a => a.Prop1 == val1 && a.Prop2 == val2;
        Func<B, bool> g1 = b => f1(b.Reference);

For expressions this doesn't work:

        Expression<Func<A, bool>> f2 = a => a.Prop1 == val1 && a.Prop2 == val2;
        Expression<Func<B, bool>> g2 = b => f2(b.Reference); // <-- Method name expected.

I would like to reuse the expression in my queries using a specification.

Like this:

Specification specification = new Specification(val1, val2)

CollectionA.Where(specification.ToExpression());

CollectionB.Where(specification.ToExpression(x => x.Reference));:


public class Specification 
{
    private readonly int val1;
    private readonly long val2;

    public Specification(int val1, long val2)
    {
        this.val1 = val1;
        this.val2 = val2;
    }

    public Expression<Func<A, bool>> ToExpression()
    {
        return x => x.Prop1 == val1 && x.Prop2 == val2;
    }

    public Expression<Func<B, bool>> ToExpression<B>(Expression<Func<B, A>> navigate)
    {
        // ?
    }
}

How to implemented this method?

Additionally I would like this to work on not only a binary 'and' expression but on any expression (i.e. any combination depth and type of parameters). (e.g. a => a.Prop1 == val1 && a.Prop2.Prop2a == val2a && a.Prop2.Prop2a == val2a) but basically it is just implementing the thing I try to do with function g2 above.


Solution

  • You can't directly call the other expression f2(b.Reference). And it would be futile to create an expression that compiles and invokes f2.

    What you actually want to do is compose the expressions. Make a new expression that represents one expression chained to the other. The expression you're missing is actually just the argument selector that gets an A from a B like this: b => b.Reference;

    Here's a handy Compose method (similar to this one) to help chain them together.

    class A
    {
        public int Prop1 = 1;
        public int Prop2 = 2;
    }
    class B
    {
        public A Reference;
    }
    
    class Program
    {
        static Expression<Func<A, C>> Compose<A, B, C>(
            Expression<Func<A, B>> fAB, Expression<Func<B, C>> fBC)
        {
            var arg = Expression.Parameter(typeof(A));
            return Expression.Lambda<Func<A, C>>(
                Expression.Invoke(fBC, Expression.Invoke(fAB, arg)), arg);
        }
    
        static void Main(string[] args)
        {
            int val1 = 1;
            int val2 = 2;
    
            Func<A, bool> f1 = a => a.Prop1 == val1 && a.Prop2 == val2;
            Func<B, bool> g1 = b => f1(b.Reference);
    
            Expression<Func<A, bool>> f2 = a => a.Prop1 == val1 && a.Prop2 == val2;
            Expression<Func<B, A>> argSelect = b => b.Reference;
            var g2 = Compose<B, A, bool>(argSelect, f2);    
    
            A objA = new A();
            B objB = new B() { Reference = objA };
            var g2Compiled = g2.Compile();
            Console.WriteLine(g2Compiled.Invoke(objB));
    
            // Demonstrate that it's connected to our local variable
            val2 = 3;
            Console.WriteLine(g2Compiled.Invoke(objB));
        }
    }