Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I share filtering logic between multiple LINQ-where clauses?

Tags:

c#

linq

Let A be a class whith some property Hello. I would like to filter collections of A instances by this property in many places. Thus I'd make somewhere a static member of type Expression<Func<A, bool>> which denotes the filtering predicate and I use it in all places where I do a filtering. (This predicate would be transformed by an ORM to some concrete DB-specific expression.)

Next. There exists a class B with property of type A. I would like to filter collection of B instances by the same logic as was used in the first case (by property Hello of class A).

Question. What is the most correct way to implement this and reduce code duplication?

My suggestion. Add three things: 1) interface IWithA with property of type A, 2) class WithA<T> implementing this interface IWithA and providing property of type T, 3) some static property of type Expression<Func<IWithA, bool>> implementing the filtering logic. Demo code is following.

    public static void Main() {
        var listOfAs = new List<A>().AsQueryable();
        var query0 = listOfAs
            .Select(a => new WithA<A> {
                A = a,
                Smth = a,
            })
            .Where(Filter);
        var listOfBs = new List<B>().AsQueryable();
        var query1 = listOfBs
            .Select(b => new WithA<B> {
                A = b.A,
                Smth = b,
            })
            .Where(Filter);
    }

    private class A {
        public int Hello { get; set; }
    }

    private class B {
        public A A { get; set; }
    }

    private interface IWithA {
        A A { get; set; }
    }

    private class WithA<T> : IWithA {
        public A A { get; set; }
        public T Smth { get; set; }
    }

    private static readonly Expression<Func<IWithA, bool>> Filter = a => a.A.Hello > 0;

The problem with this approach: 1) one must always make Select(x => new WithA<X> { ... }), 2) the ORM may not support this.

About the answer. I am satisfied with the accepted answer (by Ivan Stoev). I think it is the best possible approach. Also it is helpful to look at the suggestion from Mihail Stancescu (see it in comments to the question). Still I do not understand the answer from user853710; may be it is useful also.

like image 678
Hoborg Avatar asked Jan 21 '16 07:01

Hoborg


1 Answers

I would create and use a helper function that converts the original Expression<A, bool> to Expression<B, bool> using the System.Linq.Expressions like this

public static class ExpressionUtils
{
    public static Expression<Func<TTarget, bool>> ConvertTo<TSource, TTarget>(this Expression<Func<TSource, bool>> source, Expression<Func<TTarget, TSource>> sourceSelector)
    {
        var body = new ParameterExpressionReplacer { source = source.Parameters[0], target = sourceSelector.Body }.Visit(source.Body);
        var lambda = Expression.Lambda<Func<TTarget, bool>>(body, sourceSelector.Parameters);
        return lambda;
    }

    class ParameterExpressionReplacer : ExpressionVisitor
    {
        public ParameterExpression source;
        public Expression target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == source ? target : base.VisitParameter(node);
        }
    }
}

Sample usage

Expression<Func<A, bool>> filterA = item => item.Hello == 2; // The original logic
var filterB = filterA.ConvertTo((B b) => b.A);

This approach doesn't require any changes to your entity model. Of course you can cache the filters in a static properties of the respective classes if you wish, but the principle is still write the logic in one place and then just use convert in other places.

like image 147
Ivan Stoev Avatar answered Oct 18 '22 04:10

Ivan Stoev