-1

Net core application. I have a generic repository pattern implemented. I am trying to implement some filtering functionality. I have the below code.

var param = Expression.Parameter(typeof(SiteAssessmentRequest), "x");
Expression<Func<SiteAssessmentRequest, bool>> query;
query = x => x.CreatedBy == request.Userid || x.AssignedTo == request.Userid;
Expression body = Expression.Invoke(query, param);
if (request.Client != null && request.Client.Length != 0)
            {
                Expression<Func<SiteAssessmentRequest, bool>> internalQuery = x => request.Client.Contains(x.Client);
                body = Expression.AndAlso(Expression.Invoke(query, param), Expression.Invoke(internalQuery, param));
            }

            if (request.CountryId != null && request.CountryId.Length != 0)
            {
                Expression<Func<SiteAssessmentRequest, bool>> internalQuery = x => request.CountryId.Contains(x.CountryId);
                body = Expression.AndAlso(Expression.Invoke(query, param), Expression.Invoke(internalQuery, param));
            }

            if (request.SiteName != null && request.SiteName.Length != 0)
            {
                Expression<Func<SiteAssessmentRequest, bool>> internalQuery = x => request.SiteName.Contains(x.SiteName);
                body = Expression.AndAlso(Expression.Invoke(query, param), Expression.Invoke(internalQuery, param));
            }

            if (request.Status != null && request.Status.Length != 0)
            {
                Expression<Func<SiteAssessmentRequest, bool>> internalQuery = x => request.Status.Contains(x.Status);
                body = Expression.AndAlso(Expression.Invoke(query, param), Expression.Invoke(internalQuery, param));
            }
            var lambda = Expression.Lambda<Func<SiteAssessmentRequest, bool>>(body, param);
var siteAssessmentRequest = await _siteAssessmentRequestRepository.GetAsync(lambda, null, x => x.Country).ConfigureAwait(false);

In the above code when I pass more than one parameter, for example, request. Status and request.SiteName I want to filter based on status and Sitename. When I see the query only one parameter appended in the query

{x => (Invoke(x => (Not(IsNullOrEmpty(x.CreatedBy)) AndAlso Not(IsNullOrWhiteSpace(x.CreatedBy))), x)
 AndAlso Invoke(x => value(Site.V1.Implementation.GetSARByFilterAr+<>c__DisplayClass12_0)
.request.Status.Contains(x.Status), x))} 

After searching so much I got below code

public static class ExpressionCombiner
    {
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> exp, Expression<Func<T, bool>> newExp)
        {
            // get the visitor
            var visitor = new ParameterUpdateVisitor(newExp.Parameters.First(), exp.Parameters.First());
            // replace the parameter in the expression just created
            newExp = visitor.Visit(newExp) as Expression<Func<T, bool>>;

            // now you can and together the two expressions
            var binExp = Expression.And(exp.Body, newExp.Body);
            // and return a new lambda, that will do what you want. NOTE that the binExp has reference only to te newExp.Parameters[0] (there is only 1) parameter, and no other
            return Expression.Lambda<Func<T, bool>>(binExp, newExp.Parameters);
        }

        class ParameterUpdateVisitor : ExpressionVisitor
        {
            private ParameterExpression _oldParameter;
            private ParameterExpression _newParameter;

            public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
            {
                _oldParameter = oldParameter;
                _newParameter = newParameter;
            }

            protected override Expression VisitParameter(ParameterExpression node)
            {
                if (object.ReferenceEquals(node, _oldParameter))
                    return _newParameter;

                return base.VisitParameter(node);
            }
        }
    }

But I am struggling to make it to work above code. In the above query, I see only status but not site name. So I want to include multiple expressions. can someone help me regarding this? Any help would be greatly appreciated. Thanks

  • Probably a duplicate of https://stackoverflow.com/a/8522600/224370 but please can you post a minimal example if it isn't. You should avoid `Invoke` and use an `ExpressionVisitor` to combine expressions rewriting the parameter. – Ian Mercer Jan 21 '21 at 06:47
  • You have to use `PredicateBuilder` from LINQKit library. – Svyatoslav Danyliv Jan 21 '21 at 07:40
  • After searching so much I found above code I pasted but I am struggling how can I use above class ExpressionCombiner to combine expression. Can someone help me to do this – Niranjan Godbole Hosmar Jan 21 '21 at 08:37
  • @NiranjanGodboleHosmar why are you using such code at all? LINQ allows you to use any expression you want. The very fact that you have to deal with such complex code should be a very strong warning that you're doing things wrong. As for repositories, if you try to create a "generic" repository on top of a higher-level ORM like EF is an ugly *anti*pattern. A DbSet is already a repository, a DbContext is a multi-entity Unit-of-Work. I suspect you look at dynamic LINQ expressions because the "generic" repository prevents you from using LINQ – Panagiotis Kanavos Jan 21 '21 at 08:41
  • Thanks Kanavos. So i should use dynamic linq expressions right since I am using generic repository pattern. – Niranjan Godbole Hosmar Jan 21 '21 at 08:44
  • If you want to dynamically combine multiple conditions with `AND`, all you have to do is chain multiple `Where()` calls, eg `if (x) { query=query.Where(...)};` – Panagiotis Kanavos Jan 21 '21 at 08:44
  • @NiranjanGodboleHosmar no, you shouldn't use either. You should explain what your actual problem is. If you use a full-featured ORM like EF or NHibernate, you shouldn't use the bad practice called a "generic" repository that is anything but - neither generic, nor a repository. – Panagiotis Kanavos Jan 21 '21 at 08:46
  • @NiranjanGodboleHosmar you should *really read* Gunnar Peipman's [No need for repositories and unit of work with Entity Framework Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/). That's not a new idea, [Repository is the new Signleton](https://ayende.com/blog/3955/repository-is-the-new-singleton) is from 2009. Compare your code to the simple `if (x) { query=query.Where(...)};`. You can't use that because the "repository" broke LINQ – Panagiotis Kanavos Jan 21 '21 at 08:47

1 Answers1

4

Let me explain core concept of LambdaExpression. LambdaExpression has 0..N parameters and Body. Body of LambdaExpression is Expression which actually we want to reuse.

Basic mistake is trying to combine lambda expressions (changed parameter names because it is how ExpressionTree works - it compares parameters by reference, not by name):

Expression<Func<Some, bool>> lambda1 = x1 => x1.Id == 10;
Expression<Func<Some, bool>> lambda2 = x2 => x2.Value == 20;
// wrong
var resultExpression = Expression.AndAlso(lambda1, lambda2);

Schematically previous wrong sample should looks like (even it will crash)

(x1 => x1.Id == 10) && (x2 => x2.Value == 20)

What we have - not desired expression but combination of "functions".

So let reuse body of LambdaExpression

// not complete
var newBody = Expression.AndAlso(lambda1.Body, lambda2.Body);

Result is more acceptable but still needs corrections:

(x1.Id == 10) && (x2.Value == 20)

Why we need correction? Because we are trying to build the following LambdaExpression

var param = Expression.Parameter(typeof(some), "e");

var newBody = Expression.AndAlso(lambda1.Body, lambda2.Body);
// still wrong
var newPredicate = Expression.Lambda<Func<Some, bool>>(newBody, param)

Result of newPredicate should looks like

e => (x1.Id == 10) && (x2.Value == 20)

As you can see we left in body parameters from previous two lambdas, which is wrong and we have to replace them with new parameter param ("e") and better to do that before combination. I'm using my implementation of replacer, which just returns desired body.

So, let's write correct lambda!

Expression<Func<Some, bool>> lambda1 = x1 => x1.Id == 10;
Expression<Func<Some, bool>> lambda2 = x2 => x2.Value == 20;

var param = Expression.Parameter(typeof(Some), "e");

var newBody = Expression.AndAlso(
  ExpressionReplacer.GetBody(lambda1, param), 
  ExpressionReplacer.GetBody(lambda2, param));

// hurray!
var newPredicate = Expression.Lambda<Func<Some, bool>>(newBody, param);

var query = query.Where(newPredicate);

After theses steps you will have desired result:

e => (e.Id == 10) && (e.Value == 20)

And ExpressionReplacer implementation

class ExpressionReplacer : ExpressionVisitor
{
    readonly IDictionary<Expression, Expression> _replaceMap;

    public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
    {
        _replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
    }

    public override Expression Visit(Expression exp)
    {
        if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
            return replacement;
        return base.Visit(exp);
    }

    public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
    {
        return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
    }

    public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
    {
        return new ExpressionReplacer(replaceMap).Visit(expr);
    }

    public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
    {
        if (lambda.Parameters.Count != toReplace.Length)
            throw new InvalidOperationException();

        return new ExpressionReplacer(Enumerable.Zip(lambda.Parameters, toReplace, (f, s) => Tuple.Create(f, s))
            .ToDictionary(e => (Expression)e.Item1, e => e.Item2)).Visit(lambda.Body);
    }
}

If you plan to work with ExpressionTree closer, I suggest to install this VS extension, which should simplify your life: Readable Expressions

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32