В предыдущем посте я показал как можно использовать обобщенное программирование при доступе к данным. С помощью пары простых методов можно заставить работать код вида

from e in context.Entity1Set.Visible()
select e;

Пример надуманный, так как предикат для определения видимости простой, и его несложно в каждом выражении записать явно. Но вместо него может быть предикат проверки прав доступа, сам по себе содержащий подзапросы, соединения и тому подобное.

Если написать что-то вроде

from e1 in context.Entity1Set.Visible()
from e2 in e1.Entity2.Visible()
select e2;

То такой код даже не скомпилируется, потому что e1.Entity2 не реализует интерфейс IQueryable<T>. Даже если бы такой ко компилировался, то наверняка отвалился бы в runtume, потому что Linq провайдер не знает что делать с методом Visible.

Проблему можно было бы решить если бы существовал простой способ поставить одно дерево выражений в другое. В языке F# есть возможность сращивания цитат (аналогов деревьев выражений для .NET), то есть подстановки одной цитаты в другую. Тогда вместо вызовов методов для коллекций можно было бы подставлять предикаты прямо в запрос. Что-то вроде такого:

from e1 in context.Entity1Set    
where Visible(e1)
from e2 in e1.Entity2
where Visible(e2)
select e2;

Но какой тип должен быть у предиката Visible? Чтобы код скомпилировался нужно чтобы Visible возвращал bool, а чтобы его можно было анализировать в runtime это должен быть тип Expression<Func<IVisible, bool>>

Идея в следующем, доработать FixupVisitor из предыдущего поста чтобы он находил в дереве выражений Expression<Func<…>> и выражение в само дерево. Чтобы такая подстановка компилировалась надо сделать метод, который будет преобразовывать типы, он же будет маркером, который скажет визитору, что надо подставить одно дерево в другое.

Нужен метод Splice (по англ. сращивание), который будет принимать Expression<Func<…,T>> и возвращать T.

public static T Splice<T,T1>
			(this Expression<Func<T1, T>> expr, T1 p1)
{
    throw new NotSupportedException();
}

Вызываться непосредственно этот метод не должен, только использоваться в expression tree.

Теперь добавим в FixupVisitor пару методов

protected override Expression VisitMethodCall
                   (MethodCallExpression node)
{
    if (!CheckSpliceMethod(node.Method))
    {
        return base.VisitMethodCall(node);
    }

    var args = node.Arguments;
    var expr = ExpressionExtensions.StripQuotes(args.First());

    //Если выражение не было подставлено непосредственно
    if (!(expr is LambdaExpression))
    {
        expr = (Expression)Expression.Lambda(expr)
                                     .Compile()
                                     .DynamicInvoke();
    }
    var lambda = expr as LambdaExpression;

    //Подстановка параметров в сращиваемое выражение
    return base.Visit(ExpressionExtensions.ReplaceAll
                        (lambda.Body, 
                         lambda.Parameters, 
                         args.Skip(1)));
}

//Проверяет что это нужный метод Splice
private bool CheckSpliceMethod(MethodInfo mi)
{
    if (mi.Name != "Splice" || mi.GetParameters().Length < 1)
        return false;
 
    var t = mi.GetParameters().First().ParameterType;

    return  t.IsGenericType &&
            t.GetGenericTypeDefinition() == typeof(Expression<>) &&
            t.GetGenericArguments()[0]
             .GetGenericArguments().Last() == mi.ReturnType;
}

Для замены формальных параметров лямбды на фактические параметры вызова метода Splice применяется  функция ReplaceAll, которая тоже реализована с помощью визитора.

class ExpressionReplacer : ExpressionVisitor
{
    Predicate<Expression> matcher;
    Func<Expression, Expression> replacer;

    public ExpressionReplacer
               (Predicate<Expression> matcher, 
                Func<Expression, Expression> replacer)
    {
        this.matcher = matcher;
        this.replacer = replacer;
    }

    public ExpressionReplacer(Expression searchFor, 
                              Expression replaceWith)
        : this(e => e == searchFor, _ => replaceWith)
    { }

    public override Expression Visit(Expression node)
    {            
        if(matcher(node))
        {
            return replacer(node);
        }

        return base.Visit(node);
    }
}

public static class ExpressionExtensions
{
    public static Expression StripQuotes(Expression expression)
    {
        if (expression.NodeType == ExpressionType.Quote)
        {
            return (expression as UnaryExpression).Operand;
        }
        else return expression;
    }

    public static Expression Replace
		      (Expression expression, 
                       Predicate<Expression> matcher, 
                       Func<Expression, Expression> replacer)
    {
        return new ExpressionReplacer(matcher, replacer)
                       .Visit(expression);
    }

    public static Expression Replace
                      (Expression expression, 
                       Expression searchFor, 
                       Expression replaceWith)
    {
        return new ExpressionReplacer(searchFor, replaceWith)
                       .Visit(expression);
    }

    public static Expression ReplaceAll
                  (Expression expression, 
                   IEnumerable<Expression> searchFor, 
                   IEnumerable<Expression> replaceWith)
    {
        return searchFor.Zip(replaceWith, Tuple.Create)
                        .Aggregate(expression, (e, p) => 
                             Replace(expression, 
                                     p.Item1, p.Item2));
    }
}

Теперь тестовый пример

static void Main(string[] args)
{
    Expression<Func<IVisible, bool>> 
	visiblePredicate = e => e.Visible;

    var context = new Model1Container();
    var q = from e1 in context.Entity1Set                    
            where visiblePredicate.Splice(e1)
            from e2 in e1.Entity2
            where visiblePredicate.Splice(e2)
            select e2;
    Console.WriteLine((q.Fix() as ObjectQuery).ToTraceString());
}

Дает результат

SELECT
[Extent1].[Id] AS [Id],
[Extent2].[Id] AS [Id1],
[Extent2].[Visible] AS [Visible],
[Extent2].[Entity1_Id] AS [Entity1_Id]
FROM  [dbo].[Entity1Set] AS [Extent1]
INNER JOIN [dbo].[Entity2Set] AS [Extent2] 
ON [Extent1].[Id] = [Extent2].[Entity1_Id]
WHERE ([Extent1].[Visible] = 1) 
    AND ([Extent2].[Visible] = 1)

Теги : Linq, .NET