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