Olá pessoALL,

Dizem que quem tem amigos tem tudo! Hoje queria iniciar esse post com um agradecimento ENORME para o Rafael Almeida também conhecido como ralms.

Estava quebrando a cabeça em como montrar filtros dinâmicos no EF de uma maneira legível e com a dica do Rafa eu consegui chegar em um resultado bem interessante.

Antes de ir para a dica, gostaria de fazer um adendo, no meu caso eu queria montar filtros dinâmicos baseados em Expression<Func<T,bool>> e não em strings. Se esse é o seu caso, ou se você precisa aplicar regras mais complexas, recomendo fortemente que você de uma olhada no projeto LINQKit.

Cenário

Com uma classe simples (POCO) que faço bind na requisição da minha api, eu gostaria de montar um filtro dinâmico.

public class GetOrdersRequest
    {
        public string OrderType { get; set; }
        public int? PartyId { get; set; }
        public bool? HasParty { get; set; }

        public Expression<Func<Order, bool>> BuildFilter()
        {
            Expression<Func<Order, bool>> result = order => true;

            if (!string.IsNullOrWhiteSpace(OrderType))
            {
                var orderTypesFilter = OrderType.Split(",", StringSplitOptions.RemoveEmptyEntries);
                var orderTypes = new List<int>();
                foreach (var strOrderType in orderTypesFilter)
                {
                    if (int.TryParse(strOrderType, out int orderType))
                    {
                        orderTypes.Add(orderType);
                    }
                }

                Expression<Func<Order, bool>> orderTypesExpression = order => orderTypes.Contains(order.OrderType);
                result = result.And(orderTypesExpression);
            }

            if (HasParty.HasValue)
            {
                if (HasParty.GetValueOrDefault())
                {
                    Expression<Func<Order, bool>> partyFilter = o => o.PartyId != null;
                    result = result.And(partyFilter);
                }
                else
                {
                    Expression<Func<Order, bool>> partyFilter = o => o.PartyId == null;
                    result = result.And(partyFilter);
                }
            }

            if (PartyId.HasValue)
            {
                Expression<Func<Order, bool>> partyFilter = o => o.PartyId == PartyId.Value;
                result = result.And(partyFilter);
            }

            return result;
        }
    }

Observem o uso do método de extenção .And(), é aqui que a mágica acontece:

public static class PredicateExpressionExtensions
    {
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b)
        {
            var parameter = a.Parameters[0];
            var visitor = new SubstExpressionVisitor();
            visitor.subst[b.Parameters[0]] = parameter;
            var body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
            return Expression.Lambda<Func<T, bool>>(body, parameter);

        }

        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b)
        {
            var parameter = a.Parameters[0];
            var visitor = new SubstExpressionVisitor();
            visitor.subst[b.Parameters[0]] = parameter;
            var body = Expression.Or(a.Body, visitor.Visit(b.Body));
            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }
    }

    internal class SubstExpressionVisitor : ExpressionVisitor
    {
        public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (subst.TryGetValue(node, out var newValue))
            {
                return newValue;
            }

            return node;
        }

    }

Para usarmos o filtro em uma query do EF, basta apenas invocar o método BuildFilter:

var query = context.Orders
             .AsNoTracking()
             .Where(search.BuildFilter())
             .OrderByDescending(x => x.OrderDate)
             .AsQueryable();

É isso aê pessoALL!
Espero ter ajudado e até a próxima.