Динамические предикаты для запросов Linq-to-Entity
Следующий запрос Linq-to-Entities работает нормально:
var query = repository.Where(r => r.YearProp1.HasValue &&
r.YearProp1 >= minYear &&
r.YearProp1 <= maxYear);
В моей базе данных имеется около дюжины столбцов, которые содержат информацию о годовом отчете (short?
тип данных). Я хочу повторно использовать одну и ту же логику Linq-to-Entities для всех этих столбцов. Что-то вроде:
Func<RepoEntity, short?> fx = GetYearPropertyFunction();
var query = repository.Where(r => fx(r).HasValue &&
fx(r) >= minYear &&
fx(r) <= maxYear);
Это приводит к ошибке:
LINQ to Entities не распознает метод 'System.Nullable`1 [System.Int16] fx (RepoEntity)', и это метод не может быть переведен в выражение хранилища.
Я понимаю, почему я получаю ошибку, но мне интересно, есть ли способ обхода, который не включает дублирование кода в десятки раз, чтобы изменить свойство, на котором работает SQL-запрос.
Я бы повторно использовал функцию более чем в одном запросе, поэтому я предполагаю, что общая версия моего вопроса такова: Есть ли способ преобразования простой функции лямбда-функции get-getter в выражение, которое может быть использовано Linq к Entities?
Ответы
Ответ 1
Создание ответа Raphaël Althaus, но добавив общий селектор, который вы изначально искали:
public static class Examples
{
public static Expression<Func<MyEntity, short?>> SelectPropertyOne()
{
return x => x.PropertyOne;
}
public static Expression<Func<MyEntity, short?>> SelectPropertyTwo()
{
return x => x.PropertyTwo;
}
public static Expression<Func<TEntity, bool>> BetweenNullable<TEntity, TNull>(Expression<Func<TEntity, Nullable<TNull>>> selector, Nullable<TNull> minRange, Nullable<TNull> maxRange) where TNull : struct
{
var param = Expression.Parameter(typeof(TEntity), "entity");
var member = Expression.Invoke(selector, param);
Expression hasValue = Expression.Property(member, "HasValue");
Expression greaterThanMinRange = Expression.GreaterThanOrEqual(member,
Expression.Convert(Expression.Constant(minRange), typeof(Nullable<TNull>)));
Expression lessThanMaxRange = Expression.LessThanOrEqual(member,
Expression.Convert(Expression.Constant(maxRange), typeof(Nullable<TNull>)));
Expression body = Expression.AndAlso(hasValue,
Expression.AndAlso(greaterThanMinRange, lessThanMaxRange));
return Expression.Lambda<Func<TEntity, bool>>(body, param);
}
}
Может использоваться несколько как оригинальный запрос, который вы искали:
Expression<Func<MyEntity, short?>> whatToSelect = Examples.SelectPropertyOne;
var query = Context
.MyEntities
.Where(Examples.BetweenNullable<MyEntity, short>(whatToSelect, 0, 30));
Ответ 2
Предикат - это сам фильтр, который должен оцениваться как bool (для того, включать или не включать его в результаты). Вы можете переделать свой метод таким образом, чтобы он выглядел следующим образом:
public static Expression<Func<RepoEntity, bool>> FitsWithinRange(int minYear, int maxYear)
{
return w => w.HasValue && w >= minYear && w <= maxYear;
}
Изменить: Ох и использовать его:
var query = repository.Where(Repository.FitsWithinRange(minYear, maxYear));
Ответ 3
Вы можете сделать что-то подобное (не уверен, что он будет работать "как есть" в объектах linq2, но если у вас есть проблема... просто скажите)
Использование
var query = <your IQueryable<T> entity>.NullableShortBetween(1, 3).ToList();
функция
public static IQueryable<T> NullableShortBetween<T>(this IQueryable<T> queryable, short? minValue, short? maxValue) where T: class
{
//item (= left part of the lambda)
var parameterExpression = Expression.Parameter(typeof (T), "item");
//retrieve all nullable short properties of your entity, to change if you have other criterias to get these "year" properties
var shortProperties = typeof (T).GetProperties().Where(m => m.CanRead && m.CanWrite && m.PropertyType == typeof(short?));
foreach (var shortProperty in shortProperties)
{
//item (right part of the lambda)
Expression memberExpression = parameterExpression;
//item.<PropertyName>
memberExpression = Expression.Property(memberExpression, shortProperty);
//item.<PropertyName>.HasValue
Expression firstPart = Expression.Property(memberExpression, "HasValue");
//item.<PropertyName> >= minValue
Expression secondPart = Expression.GreaterThanOrEqual(memberExpression, Expression.Convert(Expression.Constant(minValue), typeof (short?)));
//item.<PropertyName> <= maxValue
var thirdPart = Expression.LessThanOrEqual(memberExpression, Expression.Convert(Expression.Constant(maxValue), typeof (short?)));
//item.<PropertyName>.HasValue && item.<PropertyName> >= minValue
var result = Expression.And(firstPart, secondPart);
//item.<PropertyName>.HasValue && item.<PropertyName> >= minValue && item.<PropertyName> <= maxValue
result = Expression.AndAlso(result, thirdPart);
//pass the predicate to the queryable
queryable = queryable.Where(Expression.Lambda<Func<T, bool>>(result, new[] {parameterExpression}));
}
return queryable;
}
EDIT: другое решение, основанное на "простом" отражении, которое "выглядит" так, как вы хотите
public static short? GetYearValue<T>(this T instance)
{
var propertyInfo = typeof(T).GetProperties().FirstOrDefault(m => m.CanRead && m.CanWrite && m.PropertyType == typeof(short?));
return propertyInfo.GetValue(instance, null) as short?;
}
Использование
var result = list.Where(item => item.GetYearValue() != null && item.GetYearValue() >= 1 && item.GetYearValue() <= 3).ToList();