Статья основана на ответ на StackOverflow .
Начну с описания проблемы, с которой столкнулся.
В базе данных есть несколько сущностей, которые необходимо отображать в виде таблиц в пользовательском интерфейсе.
Entity Framework используется для доступа к базе данных.
Для этих таблиц существуют фильтры на основе полей этих сущностей.
Вам нужно написать код для фильтрации сущностей по параметрам.
Например, есть две сущности: Пользователь и Продукт.
Допустим, нам нужно фильтровать пользователей по имени и товары по названию.public class User { public int Id { get; set; } public string Name { get; set; } } public class Product { public int Id { get; set; } public string Name { get; set; } }
Мы пишем методы для фильтрации каждой сущности.
public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text)
{
return users.Where(user => user.Name.Contains(text));
}
public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text)
{
return products.Where(product => product.Name.Contains(text));
}
Сразу замечаем, что эти два метода практически идентичны и отличаются лишь свойством сущности, по которой фильтруются данные.
Если у нас десятки сущностей, каждая из которых имеет десятки полей, требующих фильтрации, то это приводит к некоторым сложностям: сложности в сопровождении кода, необдуманному копированию и, как следствие, медленной разработке и высокой вероятности ошибок.
Перефразируя Фаулера, начинает пахнуть .
Вместо дублирования кода хотелось бы написать что-то более универсальное.
Например: public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text)
{
return FilterContainsText(users, user => user.Name, text);
}
public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text)
{
return FilterContainsText(products, propduct => propduct.Name, text);
}
public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities,
Func<TEntity, string> getProperty, string text)
{
return entities.Where(entity => getProperty(entity).
Contains(text));
}
К сожалению, если мы попытаемся отфильтровать public void TestFilter()
{
using (var context = new Context())
{
var filteredProducts = FilterProductsByName(context.Products, "name").
ToArray();
}
}
то мы получаем ошибку «Тестовый метод ExpressionTests.ExpressionTest.TestFilter вызвал исключение: System.NotSupportedException : Тип узла выражения LINQ. «Вызов» не поддерживается в LINQ to Entities».
Потому что
Выражения
Попробуем разобраться, что пошло не так.Метод Where принимает параметр типа Expression. > .
Те.
Linq работает не с делегатами, а с деревьями выражений, которые используются для построения SQL-запросов.
Выражение описывает узел синтаксическое дерево .
Чтобы лучше понять, как они работают, рассмотрим выражение, которое проверяет, что имя равно строке.
Expression<Func<Product, bool>> expected = product => product.Name == "target";
При отладке можно увидеть структуру этого выражения (ключевые свойства отмечены красным)
Вот так выглядит дерево
Дело в том, что когда мы передаем делегат в качестве параметра, формируется другое дерево, в котором вместо обращения к свойству сущности вызывается метод Invoke параметра (делегата).
Когда Linq пытается построить SQL-запрос к этому дереву, он не знает, как интерпретировать метод Invoke, и выдает исключение NotSupportedException. Таким образом, наша задача — заменить доступ к свойству сущности (той части дерева, которая выделена красным) выражением, передаваемым через параметр.
Давай попробуем: Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter(product) == "target"
Теперь мы видим ошибку «Ожидается имя метода» уже на этапе компиляции.
Проблема в том, что выражение — это класс, представляющий узлы.
синтаксическое дерево , а не сам делегат и не может быть вызван напрямую.
Теперь основная задача — найти способ сформировать выражение, передав в качестве параметра другое выражение.
Посетитель
После некоторого поиска в Google я нашел решение аналогичной проблемы на Переполнение стека .Есть специальный класс для работы с выражениями ВыражениеПосетитель , который использует шаблон Посетитель .
Его суть в том, что он обходит все узлы дерева выражений в том порядке, в котором анализируется синтаксическое дерево, и позволяет их изменить или вместо этого вернуть другой узел.
Если ни сам узел, ни его дочерние узлы не изменились, то возвращается исходное выражение.
Те.
Наследуя класс ExpressionVisitor, мы можем заменить любой узел дерева выражением, которое мы передаем через параметр.
Таким образом, нам необходимо разместить в дереве какой-то узел-метку, который при обходе мы заменим на параметр.
Для этого мы напишем метод расширения, который будет имитировать вызов выражения и будет являться меткой.
public static class ExpressionExtension
{
public static TFunc Call<TFunc>(this Expression<TFunc> expression)
{
throw new InvalidOperationException("This method should never be called. It is a marker for replacing.");
}
}
Теперь мы можем вставлять одно выражение в другое Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product) == "target";
Осталось написать посетителя, который заменит вызов метода Call в дереве выражений на его параметр: public class SubstituteExpressionCallVisitor : ExpressionVisitor
{
private readonly MethodInfo _markerDesctiprion;
public SubstituteExpressionCallVisitor()
{
_markerDesctiprion =
typeof(ExpressionExtension).
GetMethod(nameof(ExpressionExtension.Call)).
GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { return Visit(ExtractExpression(node)); } return base.VisitMethodCall(node); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target = node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).
Compile().
DynamicInvoke();
}
private bool IsMarker(MethodCallExpression node)
{
return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == _markerDesctiprion;
}
}
Теперь мы можем заменить наш маркер.
public static Expression<TFunc> SubstituteMarker<TFunc>(this Expression<TFunc> expression)
{
var visitor = new SubstituteExpressionCallVisitor();
return (Expression<TFunc>)visitor.Visit(expression);
}
Expression<Func<Product, string>> propertyGetter = product => product.Name;
Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product).
Contains("123");
Expression<Func<Product, bool>> finalFilter = filter.SubstituteMarker();
При отладке мы видим, что выражение получилось не совсем таким, как мы ожидали.
Фильтр по-прежнему содержит метод Invoke.
Проблема в том, что выраженияparameterGetter и FinalFilter используют два разных аргумента.
Поэтому нам нужно заменить аргумент в параметре Getter аргументом из FinalFilter. Для этого напишем еще одного посетителя.
В результате мы получаем такой код: public class SubstituteParameterVisitor : ExpressionVisitor
{
private readonly LambdaExpression _expressionToVisit;
private readonly Dictionary<ParameterExpression, Expression> _substitutionByParameter;
public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit)
{
_expressionToVisit = expressionToVisit;
_substitutionByParameter = expressionToVisit
.
Parameters .
Select((parameter, index) => new {Parameter = parameter, Index = index}) .
ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(ParameterExpression node) { Expression substitution; if (_substitutionByParameter.TryGetValue(node, out substitution)) { return Visit(substitution); } return base.VisitParameter(node); } } public class SubstituteExpressionCallVisitor : ExpressionVisitor { private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion = typeof(ExpressionExtensions) .
GetMethod(nameof(ExpressionExtensions.Call)) .
GetGenericMethodDefinition(); } protected override Expression VisitInvocation(InvocationExpression node) { var isMarkerCall = node.Expression.NodeType == ExpressionType.Call && IsMarker((MethodCallExpression) node.Expression); if (isMarkerCall) { var parameterReplacer = new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression)); var target = parameterReplacer.Replace(); return Visit(target); } return base.VisitInvocation(node); } private LambdaExpression Unwrap(MethodCallExpression node) { var target = node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).
Compile().
DynamicInvoke();
}
private bool IsMarker(MethodCallExpression node)
{
return node.Method.IsGenericMethod &&
node.Method.GetGenericMethodDefinition() == _markerDesctiprion;
}
}
Теперь все работает как надо и мы наконец можем написать наш метод фильтрации.
public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities, Expression<Func<TEntity, string>> getProperty, string text)
{
Expression<Func<TEntity, bool>> filter = entity => getProperty.Call()(entity).
Contains(text);
return entities.Where(filter.SubstituteMarker());
}
Послесловие
Подход с подстановкой выражений можно использовать не только для фильтрации, но и для сортировки и вообще для любых запросов к базе данных.Этот метод также позволяет хранить выражения вместе с бизнес-логикой отдельно от самих запросов к базе данных.
Вы можете увидеть полный код на github .
Теги: #entityframework #выражения #LINQ #c#.
net #программирование #.
NET #Алгоритмы #C++
-
Интерактивная Карта Внедрения Ipv6
19 Oct, 24 -
10 Мифов О Бешенстве
19 Oct, 24 -
Какой Хабралент Вы Бы Хотели Видеть?
19 Oct, 24