Создавать LINQ для сущностей OrderBy выражение на лету
Я пытаюсь добавить выражение orderby на лету. Но когда выполняется следующий запрос, я получаю следующее исключение:
System.NotSupportedException: невозможно для создания постоянного значения типа 'Тип закрытия'. Только примитивные типы (например, Int32, String и Guid) поддерживаются в этом контексте.
Странная вещь: я запрашиваю только эти примитивные типы.
string sortBy = HttpContext.Current.Request.QueryString["sidx"];
ParameterExpression prm = Expression.Parameter(typeof(buskerPosting), "posting");
Expression orderByProperty = Expression.Property(prm, sortBy);
// get the paged records
IQueryable<PostingListItemDto> query =
(from posting in be.buskerPosting
where posting.buskerAccount.cmsMember.nodeId == m.Id
orderby orderByProperty
//orderby posting.Created
select new PostingListItemDto { Set = posting }).Skip<PostingListItemDto>((page - 1) * pageSize).Take<PostingListItemDto>(pageSize);
Надеюсь, кто-то может пролить свет на это!
Ответы
Ответ 1
В основном вы не можете использовать выражения запроса, подобные этому, из-за того, как они были переведены. Однако вы можете сделать это явно с помощью методов расширения:
string sortBy = HttpContext.Current.Request.QueryString["sidx"];
ParameterExpression prm = Expression.Parameter(typeof(buskerPosting), "posting");
Expression orderByProperty = Expression.Property(prm, sortBy);
// get the paged records
IQueryable<PostingListItemDto> query = be.buskerPosting
.Where(posting => posting.buskerAccount.cmsMember.nodeId == m.Id)
.OrderBy(orderByExpression)
.Select(posting => new PostingListItemDto { Set = posting })
.Skip<PostingListItemDto>((page - 1) * pageSize)
.Take<PostingListItemDto>(pageSize);
Сложный бит получает правильный тип дерева выражений - это будет сделано в edit:)
EDIT: редактирование будет несколько задерживаться по различным причинам. В принципе, вам может потребоваться вызвать общий метод с использованием отражения, так как Queryable.OrderBy
требуется общий Expression<Func<TSource, TKey>>
, и, хотя похоже, что вы знаете тип источника во время компиляции, вы можете не знать тип ключа. Если вы знаете, что он всегда будет заказывать (скажем) int, вы можете использовать:
Expression orderByProperty = Expression.Property(prm, sortBy);
var orderByExpression = Expression.Lambda<Func<buskerPosting, int>>
(orderByProperty, new[] { prm });
EDIT: Хорошо, похоже, что у меня было время. Вот короткий пример вызова OrderBy
с использованием отражения:
using System;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;
public class Test
{
static void Main()
{
string[] names = { "Jon", "Holly", "Tom", "Robin", "Will" };
var query = names.AsQueryable();
query = CallOrderBy(query, "Length");
foreach (var name in query)
{
Console.WriteLine(name);
}
}
private static readonly MethodInfo OrderByMethod =
typeof(Queryable).GetMethods()
.Where(method => method.Name == "OrderBy")
.Where(method => method.GetParameters().Length == 2)
.Single();
public static IQueryable<TSource> CallOrderBy<TSource>
(IQueryable<TSource> source, string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(TSource), "posting");
Expression orderByProperty = Expression.Property(parameter, propertyName);
LambdaExpression lambda = Expression.Lambda(orderByProperty, new[] { parameter });
Console.WriteLine(lambda);
MethodInfo genericMethod = OrderByMethod.MakeGenericMethod
(new[] { typeof(TSource), orderByProperty.Type });
object ret = genericMethod.Invoke(null, new object[] {source, lambda});
return (IQueryable<TSource>) ret;
}
}
Вы можете легко реорганизовать CallOrderBy
в метод расширения (например, OrderByProperty
) следующим образом:
public static class ReflectionQueryable
{
private static readonly MethodInfo OrderByMethod =
typeof(Queryable).GetMethods()
.Where(method => method.Name == "OrderBy")
.Where(method => method.GetParameters().Length == 2)
.Single();
public static IQueryable<TSource> OrderByProperty<TSource>
(this IQueryable<TSource> source, string propertyName)
{
ParameterExpression parameter = Expression.Parameter(typeof(TSource), "posting");
Expression orderByProperty = Expression.Property(parameter, propertyName);
LambdaExpression lambda = Expression.Lambda(orderByProperty, new[] { parameter });
Console.WriteLine(lambda);
MethodInfo genericMethod = OrderByMethod.MakeGenericMethod
(new[] { typeof(TSource), orderByProperty.Type });
object ret = genericMethod.Invoke(null, new object[] {source, lambda});
return (IQueryable<TSource>) ret;
}
}
Затем ваш исходный код будет выглядеть следующим образом:
string sortBy = HttpContext.Current.Request.QueryString["sidx"];
// get the paged records
IQueryable<PostingListItemDto> query = be.buskerPosting
.Where(posting => posting.buskerAccount.cmsMember.nodeId == m.Id)
.OrderByProperty(sortBy)
.Select(posting => new PostingListItemDto { Set = posting })
.Skip<PostingListItemDto>((page - 1) * pageSize)
.Take<PostingListItemDto>(pageSize);
(Извинения за форматирование с использованием горизонтальных полос прокрутки... Я буду переформатировать позже, если кто-нибудь позаботится. Или вы могли бы сделать это для меня, если у вас будет достаточно репутации;)
Ответ 2
Я хотел поделиться своей реализацией с использованием ответа Джона выше в качестве отправной точки. В этом случае вместо сортировки по имени свойства строки, поступающему из уровня представления (поскольку название этого вопроса не является конкретным об этом), я создаю слой данных Entity Framework и хочу разрешить его потребителю указать порядок по свойствам в виде лямбда-выражений. И.Е. Вместо того, чтобы проходить "sidx"
, я хотел использовать p => p.sidx
. Я также хотел иметь возможность передавать неограниченное количество заказов по свойствам и иметь возможность указывать восходящий или нисходящий порядок.
Ну, мой метод может принимать такое лямбда-выражение как тип Expression<Func<T, object>>
. Это позволяет мне называть его так, как я хочу, но проблема заключается в том, что Entity Framework не может преобразовать выражение в SQL, если не задан второй типичный параметр. Для метода расширения OrderBy требуются два общих параметра: T - тип, к которому принадлежит свойство, и TKey - тип возвращаемого свойства. Итак, первым шагом было модифицировать пример Jon для преобразования заданного Expression<Func<T, object>>
в Expression<Func<T, Tkey>>
(как только мы работаем в контексте запроса, мы можем определить тип TKey
):
internal static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> source, Expression<Func<T, object>> sortExp)
{
//We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
//in order for Entity Framework to be able to translate it to SQL
MemberExpression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExp.Body) as MemberExpression;
ParameterExpression sourceParam = sortExp.Parameters[0];
LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
MethodInfo orderByMethod = OrderByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
//Call OrderBy or OrderByDescending on the source IQueryable<T>
return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
}
Как я уже говорил, я хочу принять неограниченное количество заказов с помощью селекторов ключей и также иметь возможность указывать восходящее или нисходящее направление, поэтому я сделал класс-оболочку для Expression<Func<T, object>>
, который я назвал DynamicSortExpression:
public class DynamicSortExpression<T>
{
/// <summary>
/// Creates a new ascending DynamicSortExpression
/// </summary>
/// <param name="keySelector">A MemberExpression identifying the property to sort on</param>
public DynamicSortExpression(Expression<Func<T, object>> keySelector) : this(keySelector, false)
{
}
public DynamicSortExpression(Expression<Func<T, object>> keySelector, bool descending)
{
this.KeySelector = keySelector;
this.Desc = descending;
}
/// <summary>
/// Gets the expression that selects the property of T to sort on
/// </summary>
public Expression<Func<T, object>> KeySelector { get; }
/// <summary>
/// Gets sort expression is in ascending or descending order
/// </summary>
public bool Desc { get; }
}
Затем я обновил метод расширения, чтобы принять этот тип, и создал перегруз для OrderBy
, который получает List<DynamicSortExpression<T>>
и добавляет их в запрос, используя метод расширения один за другим. Вот окончательный результат:
public static class Extensions
{
private static readonly MethodInfo OrderByMethod = typeof(Queryable).GetMethods()
.Where(method => method.Name == "OrderBy")
.Where(method => method.GetParameters().Length == 2)
.Single();
private static readonly MethodInfo OrderByDescMethod = typeof(Queryable).GetMethods()
.Where(method => method.Name == "OrderByDescending")
.Where(method => method.GetParameters().Length == 2)
.Single();
private static readonly MethodInfo ThenByMethod = typeof(Queryable).GetMethods()
.Where(method => method.Name == "ThenBy")
.Where(method => method.GetParameters().Length == 2)
.Single();
private static readonly MethodInfo ThenByDescMethod = typeof(Queryable).GetMethods()
.Where(method => method.Name == "ThenByDescending")
.Where(method => method.GetParameters().Length == 2)
.Single();
internal static IQueryable<T> OrderBy<T>(this IQueryable<T> sourceQuery, List<DynamicSortExpression<T>> orderBy)
{
bool isFirst = true;
foreach (var sortExpression in orderBy)
{
if (isFirst)
{
sourceQuery = sourceQuery.OrderByDynamic(sortExpression);
isFirst = false;
}
else
sourceQuery = sourceQuery.ThenByDynamic(sortExpression);
}
return sourceQuery;
}
internal static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> source, DynamicSortExpression<T> sortExpression)
{
//We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
//in order for Entity Framework to be able to translate it to SQL
MemberExpression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExpression.KeySelector.Body) as MemberExpression;
ParameterExpression sourceParam = sortExpression.KeySelector.Parameters[0];
LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
MethodInfo orderByMethod = sortExpression.Desc ?
OrderByDescMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type }) :
OrderByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
//Call OrderBy or OrderByDescending on the source IQueryable<T>
return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
}
internal static IQueryable<T> ThenByDynamic<T>(this IQueryable<T> source, DynamicSortExpression<T> sortExpression)
{
//We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
//in order for Entity Framework to be able to translate it to SQL
Expression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExpression.KeySelector.Body) as MemberExpression;
ParameterExpression sourceParam = sortExpression.KeySelector.Parameters[0];
LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
MethodInfo orderByMethod = sortExpression.Desc ?
ThenByDescMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type }) :
ThenByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
//Call OrderBy or OrderByDescending on the source IQueryable<T>
return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
}
}
Теперь мой уровень данных может иметь такой метод, как List<T> GetList(Expression<Func<T, bool>> where, params DynamicSortExpression<T>[] orderBy)
, который можно назвать как
new MyClass<Person>().GetList(p => p.FirstName == "Billy", //where clause
new DynamicSortExpression<T>(p => p.FirstName),
new DynamicSortExpression<T>(p => p.LastName, true));
Метод RemoveConvert
- это то, что я вырвал из исходного кода EntityFramework, чтобы рекурсивно удалить Convert calls from MemberExpression:
internal static Expression RemoveConvert(Expression expression)
{
System.Diagnostics.Debug.Assert(expression != null);
while ((expression != null)
&& (expression.NodeType == ExpressionType.Convert
|| expression.NodeType == ExpressionType.ConvertChecked))
{
expression = RemoveConvert(((UnaryExpression)expression).Operand);
}
return expression;
}
Надеюсь, это полезно! Спасибо, Джон!