Database
 sql >> Base de Dados >  >> RDS >> Database

Eliminando a duplicação de expressões where no aplicativo


Suponha que você tenha produtos e categorias. Um cliente diz que é necessário usar outros processos de negócios para as categorias com valor de classificação superior a 50. Você tem uma experiência sólida e entende que amanhã esse valor pode ser diferente – 127,37. Como você deseja evitar essa situação, você escreve o código da seguinte maneira:


public class Category : HasIdBase<int>
    {
        public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50;

       //...
    }

    var niceCategories = db.Query<Category>.Where(Category.NiceRating);

Infelizmente, isso não funcionará se você precisar selecionar produtos das categorias correspondentes. NiceRating tem o tipo Expression>. No caso de Produto, você precisará usar Expression>.

Assim, precisamos converter Expression> em Expression>.
 public class Product: HasIdBase<int>
    {
        public virtual Category Category { get; set; }

       //...
    }

    var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating);

Felizmente, é bem fácil!
 // In fact, we implement a composition of statements, 
         // which returns the statement matching the composition of target functions 
     public static Expression<Func<TIn, TOut>> Compose<TIn, TInOut, TOut>(
             this Expression<Func<TIn, TInOut>> input,
             Expression<Func<TInOut, TOut>> inOutOut)
        {
            // this is the X parameter => blah-blah. For a lambda, we need null
            var param = Expression.Parameter(typeof(TIn), null);
            // we get an object, to which this statement is applied
            var invoke = Expression.Invoke(input, param);
            // and execute “get an object and apply its statement”
            var res = Expression.Invoke(inOutOut, invoke);
            
            // return a lambda of the required type 
            return Expression.Lambda<Func<TIn, TOut>>(res, param);
        }
        
        // add an “advanced” variant of Where
        public static IQueryable<T> Where<T, TParam>(this IQueryable<T> queryable,
            Expression<Func<T, TParam>> prop, Expression<Func<TParam, bool>> where)
        {
            return queryable.Where(prop.Compose(where));
        }
	
        // check
	[Fact]
	public void AdvancedWhere_Works()
	{
		var product = new Product(new Category() {Rating = 700}, "Some Product", 100500);
		var q = new[] {product}.AsQueryable();

		var values = q.Where(x => x.Category, Category.NiceRating).ToArray();
		Assert.Equal(700, values[0].Category.Rating);
	}

Esta é a implementação da composição de instruções no LinqKit. No entanto, o Entity Framework não funciona com InvokeExpression e lança NotSupportedException. Você sabia que o LINQ tem desvantagens? Para contornar essa restrição, no LinqKit usamos um método de extensão AsExpandable. Pete Montgomery descreveu esse problema em seu blog. Sua versão do Predicate Builder funciona tanto para IEnumerable quanto para IQueryable.

Aqui está o código como está.
public static class PredicateBuilder
{
    /// <summary>
    /// Creates a predicate that evaluates to true.
    /// </summary>
    public static Expression<Func<T, bool>> True<T>() { return param => true; }
 
    /// <summary>
    /// Creates a predicate that evaluates to false.
    /// </summary>
    public static Expression<Func<T, bool>> False<T>() { return param => false; }
 
    /// <summary>
    /// Creates a predicate expression from the specified lambda expression.
    /// </summary>
    public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; }
 
    /// <summary>
    /// Combines the first predicate with the second using the logical "and".
    /// </summary>
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.AndAlso);
    }
 
    /// <summary>
    /// Combines the first predicate with the second using the logical "or".
    /// </summary>
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.OrElse);
    }
 
    /// <summary>
    /// Negates the predicate.
    /// </summary>
    public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
    {
        var negated = Expression.Not(expression.Body);
        return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
    }
 
    /// <summary>
    /// Combines the first expression with the second using the specified merge function.
    /// </summary>
    static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
    {
        // zip parameters (map from parameters of second to parameters of first)
        var map = first.Parameters
            .Select((f, i) => new { f, s = second.Parameters[i] })
            .ToDictionary(p => p.s, p => p.f);
 
        // replace parameters in the second lambda expression with the parameters in the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
 
        // create a merged lambda expression with parameters from the first expression
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }
 
    class ParameterRebinder : ExpressionVisitor
    {
        readonly Dictionary<ParameterExpression, ParameterExpression> map;
 
        ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
        {
            this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
        }
 
        public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
        {
            return new ParameterRebinder(map).Visit(exp);
        }
 
        protected override Expression VisitParameter(ParameterExpression p)
        {
            ParameterExpression replacement;
 
            if (map.TryGetValue(p, out replacement))
            {
                p = replacement;
            }
 
            return base.VisitParameter(p);
        }
    }
}