Combine Multiple Linq Expressions [duplicate]
Asked Answered
C

1

5

I'm in the process of refactoring some code, attempting to make it more self-documenting. The current code has a query over an OData service which looks like this:

return context.MessageLog.Where
(
    x => 
    (
        x.Status == MessageStatus.Success 
        || x.Status == MessageStatus.Failure
    ) 
    && x.Direction == MessageDirection.Inbound 
    && x.ResponseDate == new DateTimeOffset(new DateTime(1900, 01, 01))
);

I'm hoping to change this to make use of Linq Expressions.
I could move all of the logic into a single expression and have code run context.MessageLog.Where(MessageIsPendingResponse);. However, I'd like to create expressions for the different conditions: MessageIsProcessed (i.e. now at success or failure state), MessageIsInbound and ResponseNotYetSent (response date is null). I could combine these with multiple where statements like so:

return context.MessageLog
    .Where(MessageLogExpression.MessageIsProcessed)
    .Where(MessageLogExpression.MessageIsInbound)
    .Where(MessageLogExpression.ResponseNotYetSent);

(MessageLogExpression being a class I use to contain these predefined expressions).

Question 1

Is this the best way to combine the satements, or does it risk filtering on the wrong field first (e.g. does Linq combine all the conditions into a single query and allow the query engine (in SQL terms) to determine the best execution plan; or are we forcing it to filter on the Status field first?

Question 2

The above is great for scenarios where we have an AND joining our expressions; but how would we do an OR? I assume there's some way to combine these, but couldn't find anything obvious. I suspect something like this exists?

return context.MessageLog.Where(new OrExpression(MessageIsSuccess,MessageIsFailure));

Question 3

Is there a good way to combine expressions within another expression defintion; e.g. something like the below code (only a version that compiles)?

public static Expression<Func<MessageLogRecord, bool>> MessageIsPendingResponse
{
    get
    {
        Expression<Func<MessageLogRecord, bool>> expr = x => 
            MessageIsProcessed(x) 
            && MessageIsInbound(x) 
            && ResponseNotYetSent(x);
        return expr;
    }
}

Addendum: Code for those expressions described above:

public class MessageLogExpression
{
    public static Expression<Func<MessageLogRecord, bool>> MessageIsProcessed
    {
        get
        {
            Expression<Func<MessageLogRecord, bool>> expr = x => 
            (
                x.Status == MessageStatus.Success 
                || x.Status == MessageStatus.Failure
            );
            return expr;
        }
    }
    public static Expression<Func<MessageLogRecord, bool>> MessageIsInbound
    {
        get
        {
            Expression<Func<MessageLogRecord, bool>> expr = x => 
                x.Direction == MessageDirection.Inbound;
            return expr;
        }
    }
    static readonly DateTimeOffset NullDate = new DateTimeOffset(new DateTime(1900, 01, 01));
    public static Expression<Func<MessageLogRecord, bool>> ResponseNotYetSent
    {
        get
        {
            Expression<Func<MessageLogRecord, bool>> expr = x => 
                x.ResponseDate == NullDate; //todo: test if this works or if I have to define the value within the expression
            return expr;
        }
    }
}
Chough answered 2/3, 2018 at 17:48 Comment(0)
C
8

About #1 - in terms of EF with Linq to Entities I'd expect that EF would create the same query regardless if I split it into multiple where conditions or have everything in one. Even if it won't - an SQL database might still produce the same query execution plan, since it has it's own optimizer.

About the other questions, we are using a helper class which is based on this blog post: http://www.albahari.com/nutshell/predicatebuilder.aspx

public static class PredicateBuilder
{
  public static Expression<Func<T, bool>> True<T>()
  {
    return (Expression<Func<T, bool>>) (input => true);
  }

  public static Expression<Func<T, bool>> False<T>()
  {
    return (Expression<Func<T, bool>>) (input => false);
  }

  public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
  {
    InvocationExpression invocationExpression = Expression.Invoke((Expression) expression2, expression1.Parameters.Cast<Expression>());
    return Expression.Lambda<Func<T, bool>>((Expression) Expression.OrElse(expression1.Body, (Expression) invocationExpression), (IEnumerable<ParameterExpression>) expression1.Parameters);
  }

  public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
  {
    InvocationExpression invocationExpression = Expression.Invoke((Expression) expression2, expression1.Parameters.Cast<Expression>());
    return Expression.Lambda<Func<T, bool>>((Expression) Expression.AndAlso(expression1.Body, (Expression) invocationExpression), (IEnumerable<ParameterExpression>) expression1.Parameters);
  }
}

This works really nice as it helps you to combine expressions easily. You can start with PredicateBuilder.True<YourEntityHere>().And(... expression1 ...).And(...)... if you want to merge OR expressions you do it similarly starting with false: PredicateBuilder.False<YourEntityHere>().Or(...)...

This means that for Q3 you can do:

public static Expression<Func<MessageLogRecord, bool>> MessageIsPendingResponse
{
    get
    {
        Expression<Func<CCI_Int_ExportLog, bool>> expr = PredicateBuilder.True<MessageLogRecord>()
            .And(MessageIsProcessed)
            .And(MessageIsInbound)
            .And(ResponseNotYetSent)
        ;
        return expr;
    }
}
Casement answered 2/3, 2018 at 18:4 Comment(3)
That's awesome; thank you. Moments after posting I came across the OOTB And and Or expression methods (msdn.microsoft.com/en-us/library/…) and thought I'd solved my own question; then realised that they weren't generic so didn't work for me... Your solution gives me what they did, but in a way that works; thank-you.Chough
@Chough FYI I've found the original blog post where we took the solution from - albahari.com/nutshell/predicatebuilder.aspx- maybe there is something more written about itCasement
Thanks for the link. FYI: Regarding my last part on building an expression of expressions (i.e. rather than just applying them to an IQueryable) I assume the code I've added to the end of the question is correct (added there as it's multi-line and if correct a part of the answer; if wrong my apologies).Chough

© 2022 - 2024 — McMap. All rights reserved.