Динамический LINQ OrderBy для IEnumerable<T> / IQueryable<T>
Я нашел пример в VS2008 Примеры для динамического LINQ, который позволяет использовать строку, похожую на sql (например, OrderBy("Name, Age DESC"))
для заказа. К сожалению, включенный метод работает только на IQueryable<T>
;. Есть ли способ получить эту функциональность на IEnumerable<T>
?
26 ответов
Просто наткнулся на этого старичка...
Чтобы сделать это без динамической библиотеки LINQ, вам просто нужен код, как показано ниже. Это охватывает наиболее распространенные сценарии, включая вложенные свойства.
Чтобы заставить его работать с IEnumerable<T>
Вы можете добавить несколько методов-оберток, которые проходят через AsQueryable
- но код ниже является ядром Expression
логика нужна.
public static IOrderedQueryable<T> OrderBy<T>(
this IQueryable<T> source,
string property)
{
return ApplyOrder<T>(source, property, "OrderBy");
}
public static IOrderedQueryable<T> OrderByDescending<T>(
this IQueryable<T> source,
string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IOrderedQueryable<T> ThenBy<T>(
this IOrderedQueryable<T> source,
string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IOrderedQueryable<T> ThenByDescending<T>(
this IOrderedQueryable<T> source,
string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IOrderedQueryable<T> ApplyOrder<T>(
IQueryable<T> source,
string property,
string methodName)
{
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach(string prop in props) {
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] {source, lambda});
return (IOrderedQueryable<T>)result;
}
Редактировать: становится веселее, если вы хотите смешать это с dynamic
- хотя учтите, что dynamic
применяется только к LINQ-to-Objects (деревья выражений для ORM и т. д. не могут реально представлять dynamic
запросы - MemberExpression
не поддерживает это). Но вот способ сделать это с LINQ-to-Objects. Обратите внимание, что выбор Hashtable
из-за благоприятной семантики блокировки:
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Runtime.CompilerServices;
static class Program
{
private static class AccessorCache
{
private static readonly Hashtable accessors = new Hashtable();
private static readonly Hashtable callSites = new Hashtable();
private static CallSite<Func<CallSite, object, object>> GetCallSiteLocked(
string name)
{
var callSite = (CallSite<Func<CallSite, object, object>>)callSites[name];
if(callSite == null)
{
callSites[name] = callSite = CallSite<Func<CallSite, object, object>>
.Create(Binder.GetMember(
CSharpBinderFlags.None,
name,
typeof(AccessorCache),
new CSharpArgumentInfo[] {
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.None,
null)
}));
}
return callSite;
}
internal static Func<dynamic,object> GetAccessor(string name)
{
Func<dynamic, object> accessor = (Func<dynamic, object>)accessors[name];
if (accessor == null)
{
lock (accessors )
{
accessor = (Func<dynamic, object>)accessors[name];
if (accessor == null)
{
if(name.IndexOf('.') >= 0) {
string[] props = name.Split('.');
CallSite<Func<CallSite, object, object>>[] arr
= Array.ConvertAll(props, GetCallSiteLocked);
accessor = target =>
{
object val = (object)target;
for (int i = 0; i < arr.Length; i++)
{
var cs = arr[i];
val = cs.Target(cs, val);
}
return val;
};
} else {
var callSite = GetCallSiteLocked(name);
accessor = target =>
{
return callSite.Target(callSite, (object)target);
};
}
accessors[name] = accessor;
}
}
}
return accessor;
}
}
public static IOrderedEnumerable<dynamic> OrderBy(
this IEnumerable<dynamic> source,
string property)
{
return Enumerable.OrderBy<dynamic, object>(
source,
AccessorCache.GetAccessor(property),
Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> OrderByDescending(
this IEnumerable<dynamic> source,
string property)
{
return Enumerable.OrderByDescending<dynamic, object>(
source,
AccessorCache.GetAccessor(property),
Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> ThenBy(
this IOrderedEnumerable<dynamic> source,
string property)
{
return Enumerable.ThenBy<dynamic, object>(
source,
AccessorCache.GetAccessor(property),
Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> ThenByDescending(
this IOrderedEnumerable<dynamic> source,
string property)
{
return Enumerable.ThenByDescending<dynamic, object>(
source,
AccessorCache.GetAccessor(property),
Comparer<object>.Default);
}
static void Main()
{
dynamic a = new ExpandoObject(),
b = new ExpandoObject(),
c = new ExpandoObject();
a.X = "abc";
b.X = "ghi";
c.X = "def";
dynamic[] data = new[] {
new { Y = a },
new { Y = b },
new { Y = c }
};
var ordered = data.OrderByDescending("Y.X").ToArray();
foreach (var obj in ordered)
{
Console.WriteLine(obj.Y.X);
}
}
}
Слишком легко без каких-либо осложнений:
- добавлять
using System.Linq.Dynamic;
на вершине. - использование
vehicles = vehicles.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();
Я нашел ответ. Я могу использовать .AsQueryable<>()
метод расширения, чтобы преобразовать мой список в IQueryable, а затем запустить динамический порядок для него.
Просто наткнулся на этот вопрос.
Используя вышеописанную реализацию ApplyOrder от Marc, я собрал метод Extension, который обрабатывает SQL-подобные строки, например:
list.OrderBy("MyProperty DESC, MyOtherProperty ASC");
Подробности можно найти здесь: http://aonnull.blogspot.com/2010/08/dynamic-sql-like-linq-orderby-extension.html
Я предполагаю, что будет полезно использовать отражение, чтобы получить любое свойство, по которому вы хотите отсортировать:
IEnumerable<T> myEnumerables
var query=from enumerable in myenumerables
where some criteria
orderby GetPropertyValue(enumerable,"SomeProperty")
select enumerable
private static object GetPropertyValue(object obj, string property)
{
System.Reflection.PropertyInfo propertyInfo=obj.GetType().GetProperty(property);
return propertyInfo.GetValue(obj, null);
}
Обратите внимание, что использование отражения значительно медленнее, чем прямой доступ к свойству, поэтому необходимо изучить производительность.
Просто опираясь на то, что сказали другие. Я обнаружил, что следующее работает довольно хорошо.
public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> input, string queryString)
{
if (string.IsNullOrEmpty(queryString))
return input;
int i = 0;
foreach (string propname in queryString.Split(','))
{
var subContent = propname.Split('|');
if (Convert.ToInt32(subContent[1].Trim()) == 0)
{
if (i == 0)
input = input.OrderBy(x => GetPropertyValue(x, subContent[0].Trim()));
else
input = ((IOrderedEnumerable<T>)input).ThenBy(x => GetPropertyValue(x, subContent[0].Trim()));
}
else
{
if (i == 0)
input = input.OrderByDescending(x => GetPropertyValue(x, subContent[0].Trim()));
else
input = ((IOrderedEnumerable<T>)input).ThenByDescending(x => GetPropertyValue(x, subContent[0].Trim()));
}
i++;
}
return input;
}
Я пытался сделать это, но у меня возникли проблемы с решением Kjetil Watnedal, потому что я не использую встроенный синтаксис linq - я предпочитаю синтаксис в стиле метода. Моя конкретная проблема заключалась в попытке выполнить динамическую сортировку с использованием пользовательских IComparer
,
Мое решение закончилось так:
Для данного запроса IQueryable, например, так:
List<DATA__Security__Team> teams = TeamManager.GetTeams();
var query = teams.Where(team => team.ID < 10).AsQueryable();
И учитывая аргумент поля сортировки во время выполнения:
string SortField; // Set at run-time to "Name"
Динамический OrderBy выглядит так:
query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField));
И это использует маленький вспомогательный метод с именем GetReflectedPropertyValue():
public static string GetReflectedPropertyValue(this object subject, string field)
{
object reflectedValue = subject.GetType().GetProperty(field).GetValue(subject, null);
return reflectedValue != null ? reflectedValue.ToString() : "";
}
И последнее - я упомянул, что хотел OrderBy
использовать обычай IComparer
- потому что я хотел сделать естественную сортировку.
Для этого я просто изменяю OrderBy
чтобы:
query = query.OrderBy(item => item.GetReflectedPropertyValue(SortField), new NaturalSortComparer<string>());
Смотрите этот пост для кода для NaturalSortComparer()
,
Я наткнулся на этот вопрос, когда искал несколько предложений Linq для нескольких заказов, и, возможно, именно это искал автор
Вот как это сделать:
var query = pets.OrderBy(pet => pet.Name).ThenByDescending(pet => pet.Age);
После долгих поисков это сработало для меня:
public static IEnumerable<TEntity> OrderBy<TEntity>(this IEnumerable<TEntity> source,
string orderByProperty, bool desc)
{
string command = desc ? "OrderByDescending" : "OrderBy";
var type = typeof(TEntity);
var property = type.GetProperty(orderByProperty);
var parameter = Expression.Parameter(type, "p");
var propertyAccess = Expression.MakeMemberAccess(parameter, property);
var orderByExpression = Expression.Lambda(propertyAccess, parameter);
var resultExpression = Expression.Call(typeof(Queryable), command,
new[] { type, property.PropertyType },
source.AsQueryable().Expression,
Expression.Quote(orderByExpression));
return source.AsQueryable().Provider.CreateQuery<TEntity>(resultExpression);
}
Используйте динамический linq
просто добавь using System.Linq.Dynamic;
И используйте это так, чтобы упорядочить все ваши столбцы:
string sortTypeStr = "ASC"; // or DESC
string SortColumnName = "Age"; // Your column name
query = query.OrderBy($"{SortColumnName} {sortTypeStr}");
Сначала установите Dynamic Tools -> Диспетчер пакетов NuGet -> Консоль диспетчера пакетов
install-package System.Linq.Dynamic
Добавить пространство имен using System.Linq.Dynamic;
Теперь вы можете использовать OrderBy("Name, Age DESC")
Вы можете добавить это:
public static IEnumerable<T> OrderBy( this IEnumerable<T> input, string queryString) {
//parse the string into property names
//Use reflection to get and sort by properties
//something like
foreach( string propname in queryString.Split(','))
input.OrderBy( x => GetPropertyValue( x, propname ) );
// I used Kjetil Watnedal's reflection example
}
GetPropertyValue
функция из ответа Кжетила Ватнедала
Вопрос был бы почему? Любая такая сортировка будет генерировать исключения во время выполнения, а не во время компиляции (как ответ D2VIANT).
Если вы имеете дело с Linq to Sql, а orderby - это дерево выражений, оно все равно будет преобразовано в SQL для выполнения.
Вы можете использовать это:
public List<Book> Books(string orderField, bool desc, int skip, int take)
{
var propertyInfo = typeof(Book).GetProperty(orderField);
return _context.Books
.Where(...)
.OrderBy(p => !desc ? propertyInfo.GetValue(p, null) : 0)
.ThenByDescending(p => desc ? propertyInfo.GetValue(p, null) : 0)
.Skip(skip)
.Take(take)
.ToList();
}
Вот еще кое-что, что я нашел интересным. Если ваш источник - DataTable, вы можете использовать динамическую сортировку без использования Dynamic Linq.
DataTable orders = dataSet.Tables["SalesOrderHeader"];
EnumerableRowCollection<DataRow> query = from order in orders.AsEnumerable()
orderby order.Field<DateTime>("OrderDate")
select order;
DataView view = query.AsDataView();
bindingSource1.DataSource = view;
ссылка: http://msdn.microsoft.com/en-us/library/bb669083.aspx (Использование DataSetExtensions)
Вот еще один способ сделать это, преобразовав его в DataView:
DataTable contacts = dataSet.Tables["Contact"];
DataView view = contacts.AsDataView();
view.Sort = "LastName desc, FirstName asc";
bindingSource1.DataSource = view;
dataGridView1.AutoResizeColumns();
Вы можете конвертировать IEnumerable в IQueryable.
items = items.AsQueryable().OrderBy("Name ASC");
Благодаря Maarten ( Запрос коллекции с использованием объекта PropertyInfo в LINQ) я получил это решение:
myList.OrderByDescending(x => myPropertyInfo.GetValue(x, null)).ToList();
В моем случае я работал над "ColumnHeaderMouseClick" (WindowsForm), поэтому просто нашел конкретный нажатый столбец и соответствующий ему PropertyInfo:
foreach (PropertyInfo column in (new Process()).GetType().GetProperties())
{
if (column.Name == dgvProcessList.Columns[e.ColumnIndex].Name)
{}
}
ИЛИ ЖЕ
PropertyInfo column = (new Process()).GetType().GetProperties().Where(x => x.Name == dgvProcessList.Columns[e.ColumnIndex].Name).First();
(убедитесь, что имена столбцов совпадают со свойствами объекта)
ура
Этот ответ является ответом на комментарии, которым нужен пример решения, предоставленного @John Sheehan - Runscope
Пожалуйста, приведите пример для остальных из нас.
в DAL (уровень доступа к данным),
IEnumerable версия:
public IEnumerable<Order> GetOrders()
{
// i use Dapper to return IEnumerable<T> using Query<T>
//.. do stuff
return orders // IEnumerable<Order>
}
IQueryable версия
public IQueryable<Order> GetOrdersAsQuerable()
{
IEnumerable<Order> qry= GetOrders();
//use the built-in extension method AsQueryable in System.Linq namespace
return qry.AsQueryable();
}
Теперь вы можете использовать версию IQueryable для привязки, например, GridView в Asp.net и использовать преимущества для сортировки (вы не можете сортировать, используя версию IEnumerable)
Я использовал Dapper в качестве ORM и собрал версию IQueryable и использовал сортировку в GridView в asp.net так легко.
Альтернативное решение использует следующий класс / интерфейс. Это не совсем динамично, но работает.
public interface IID
{
int ID
{
get; set;
}
}
public static class Utils
{
public static int GetID<T>(ObjectQuery<T> items) where T:EntityObject, IID
{
if (items.Count() == 0) return 1;
return items.OrderByDescending(u => u.ID).FirstOrDefault().ID + 1;
}
}
Вы можете определить словарь из строки в Func<> следующим образом:
Dictionary<string, Func<Item, object>> SortParameters = new Dictionary<string, Func<Item, object>>()
{
{"Rank", x => x.Rank}
};
И используйте это так:
yourList.OrderBy(SortParameters["Rank"]);
В этом случае вы можете динамически сортировать по строке.
Вы можете сделать это для нескольких заказов
IOrderedEnumerable<JToken> sort;
if (query.OrderBys[0].IsDESC)
{
sort = jarry.OrderByDescending(r => (string)r[query.OrderBys[0].Key]);
}
else
{
sort = jarry.OrderBy(r =>
(string) r[query.OrderBys[0].Key]);
}
foreach (var item in query.OrderBys.Skip(1))
{
if (item.IsDESC)
{
sort = sort.ThenByDescending(r => (string)r[item.Key]);
}
else
{
sort = sort.ThenBy(r => (string)r[item.Key]);
}
}
Преобразуйте List в IEnumerable или Iquerable, добавьте с использованием пространства имен System.LINQ.Dynamic, после чего вы можете упомянуть имена свойств в строке, разделенной запятыми, в метод OrderBy, который по умолчанию поставляется из System.LINQ.Dynamic.
На простые вопросы должны быть простые ответы - все, что вам нужно, это два метода расширения:
public static IOrderedEnumerable<TSource> OrderByDynamic<TSource, TKey>(this IEnumerable<TSource> source, bool isDescending, Func<TSource, TKey> keySelector) {
if (isDescending) {
return source.OrderByDescending<TSource, TKey>(keySelector);
} else {
return source.OrderBy<TSource, TKey>(keySelector);
}
}
public static IOrderedEnumerable<TSource> OrderByDynamic<TSource, TKey>(this IEnumerable<TSource> source, bool isDescending, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) {
if (isDescending) {
return source.OrderByDescending<TSource, TKey>(keySelector,comparer);
} else {
return source.OrderBy<TSource, TKey>(keySelector,comparer);
}
}
Использование:
var orderedList = UnorderedList.OrderByDynamic(isDescending, x => x.MySortValue).ToList();
Я могу сделать это с помощью кода ниже. Нет необходимости писать длинный и сложный код.
protected void sort_array(string field_name, string asc_desc)
{
objArrayList= Sort(objArrayList, field_name, asc_desc);
}
protected List<ArrayType> Sort(List<ArrayType> input, string property, string asc_desc)
{
if (asc_desc == "ASC")
{
return input.OrderBy(p => p.GetType()
.GetProperty(property)
.GetValue(p, null)).ToList();
}
else
{
return input.OrderByDescending(p => p.GetType()
.GetProperty(property)
.GetValue(p, null)).ToList();
}
}
С Net6 и EF
.AsQueryable().OrderBy((ColumnOrder.Column, ColumnOrder.Dir));
Если вы используете спецификацию (например, спецификацию Ardalis )
using Microsoft.EntityFrameworkCore;
namespace TestExtensions;
public static class IQueryableExtensions
{
public static void ApplyOrder<T>(ISpecificationBuilder<T> query, string propertyName, bool ascendingOrder)
{
if (ascendingOrder)
query.OrderBy(T => EF.Property<object>(T!, propertyName));
else
query.OrderByDescending(T => EF.Property<object>(T!, propertyName));
}
}
var result1 = lst.OrderBy(a=>a.Name);// for ascending order.
var result1 = lst.OrderByDescending(a=>a.Name);// for desc order.