В предыдущем посте я показал как можно использовать обобщенное программирование при доступе к данным. С помощью пары простых методов можно заставить работать код вида
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)