Пейджинг с LINQ для объектов

Как бы вы реализовали разбиение на страницы в запросе LINQ? На самом деле пока я был бы удовлетворен, если бы можно было имитировать функцию sql TOP. Однако я уверен, что потребность в полной поддержке пейджинга в любом случае возникнет раньше.

var queryResult = from o in objects
                  where ...
                  select new
                      {
                         A = o.a,
                         B = o.b
                      }
                   ????????? TOP 10????????

person user256890    schedule 04.03.2010    source источник


Ответы (10)


Вы ищете методы расширения Skip и Take. Skip проходит мимо первых N элементов результата, возвращая остаток; Take возвращает первые N элементов результата, отбрасывая все оставшиеся элементы.

Дополнительную информацию об использовании этих методов см. В MSDN: http://msdn.microsoft.com/en-us/library/bb386988.aspx.

Предполагая, что вы уже учитываете, что pageNumber должен начинаться с 0 (уменьшение на 1, как предлагается в комментариях), вы можете сделать это следующим образом:

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * pageNumber)
  .Take(numberOfObjectsPerPage);

В противном случае, если pageNumber основан на 1 (как предлагает @Alvin)

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * (pageNumber - 1))
  .Take(numberOfObjectsPerPage);
person David Pfeffer    schedule 04.03.2010
comment
Должен ли я использовать тот же метод для SQL с огромной базой данных, будет ли он сначала помещать всю таблицу в память, а затем отбрасывать ненужные? - person user256890; 04.03.2010
comment
Если вас интересует, что происходит под капотом, кстати, большинство драйверов баз данных LINQ предоставляют способ получения отладочной выходной информации для фактического выполняемого SQL. - person David Pfeffer; 04.03.2010
comment
Роб Конери написал в блоге о классе PagedList ‹T›, который может помочь вам начать работу. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt - person jrotello; 04.03.2010
comment
это приведет к пропуску первой страницы, если pageNumber не отсчитывается от нуля (0). если pageNumber начинается с 1, используйте этот .Skip (numberOfObjectsPerPage * (pageNumber - 1)) - person Alvin; 07.02.2014
comment
Каким будет полученный в результате SQL-запрос, который попадет в базу данных? - person Faiz; 24.02.2014
comment
Хранилище данных SQL Azure не поддерживает метод пропуска (внутреннее использование предложения OFFSET), но ответ Lukazoid ниже работает. - person Michael Freidgeim; 21.08.2016
comment
Поддержка Azure SQL Пропустите прямо сейчас. Протестировано без проблем. - person Tony Dong; 19.05.2021

Использование Skip и Take - определенно правильный путь. Если бы я реализовывал это, я бы, вероятно, написал свой собственный метод расширения для обработки разбиения на страницы (чтобы сделать код более читаемым). Конечно, реализация может использовать Skip и Take:

static class PagingUtils {
  public static IEnumerable<T> Page<T>(this IEnumerable<T> en, int pageSize, int page) {
    return en.Skip(page * pageSize).Take(pageSize);
  }
  public static IQueryable<T> Page<T>(this IQueryable<T> en, int pageSize, int page) {
    return en.Skip(page * pageSize).Take(pageSize);
  }
}

Класс определяет два метода расширения - один для IEnumerable и один для IQueryable, что означает, что вы можете использовать его как с LINQ to Objects, так и с LINQ to SQL (при написании запроса к базе данных компилятор выберет версию IQueryable).

В зависимости от ваших требований к подкачке вы также можете добавить некоторое дополнительное поведение (например, для обработки отрицательного значения pageSize или page). Вот пример того, как вы могли бы использовать этот метод расширения в своем запросе:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);
person Tomas Petricek    schedule 04.03.2010
comment
Я считаю, что это вернет весь набор результатов, а затем отфильтрует в памяти, а не на сервере. Огромное снижение производительности базы данных, если это SQL. - person jvenema; 04.03.2010
comment
@jvenema Вы правы. Так как здесь используется интерфейс IEnumerable, а не IQueryable, это потянет за собой всю таблицу базы данных, что существенно снизит производительность. - person David Pfeffer; 04.03.2010
comment
Вы, конечно, можете легко добавить перегрузку для IQueryable, чтобы он работал и с запросами к базе данных (я отредактировал ответ и добавил его). Немного прискорбно, что вы не можете написать код полностью универсальным способом (в Haskell это было бы возможно с классами типов). В исходном вопросе упоминался LINQ to Objects, поэтому я написал только одну перегрузку. - person Tomas Petricek; 04.03.2010
comment
Я как раз думал об этом сам. Я немного удивлен, что это не входит в стандартную реализацию. Спасибо за образец кода! - person Michael Richardson; 25.10.2013
comment
Я думаю, что пример должен быть: общедоступный статический IQueryable ‹T› Page ‹T› (... и т. Д. - person David Talbot; 13.08.2014
comment
Хранилище данных SQL Azure не поддерживает метод пропуска (внутреннее использование предложения OFFSET), но ответ Lukazoid ниже работает. - person Michael Freidgeim; 21.08.2016

Вот мой эффективный подход к разбиению на страницы при использовании LINQ для объектов:

public static IEnumerable<IEnumerable<T>> Page<T>(this IEnumerable<T> source, int pageSize)
{
    Contract.Requires(source != null);
    Contract.Requires(pageSize > 0);
    Contract.Ensures(Contract.Result<IEnumerable<IEnumerable<T>>>() != null);

    using (var enumerator = source.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            var currentPage = new List<T>(pageSize)
            {
                enumerator.Current
            };

            while (currentPage.Count < pageSize && enumerator.MoveNext())
            {
                currentPage.Add(enumerator.Current);
            }
            yield return new ReadOnlyCollection<T>(currentPage);
        }
    }
}

Затем это можно использовать так:

var items = Enumerable.Range(0, 12);

foreach(var page in items.Page(3))
{
    // Do something with each page
    foreach(var item in page)
    {
        // Do something with the item in the current page       
    }
}

Никакой ерунды Skip и Take, которая будет крайне неэффективной, если вас интересуют несколько страниц.

person Lukazoid    schedule 06.03.2014
comment
Он работает в Entity Framework с хранилищем данных SQL Azure, которое не поддерживает метод Skip (внутреннее использование предложения OFFSET) - person Michael Freidgeim; 21.08.2016
comment
Это просто нужно было украсть и положить в мою общую библиотеку, спасибо! Я просто переименовал метод в Paginate, чтобы устранить двусмысленность noun vs verb. - person Gabrielius; 16.09.2016

Не знаю, поможет ли это кому-нибудь, но я нашел это полезным для своих целей:

private static IEnumerable<T> PagedIterator<T>(IEnumerable<T> objectList, int PageSize)
{
    var page = 0;
    var recordCount = objectList.Count();
    var pageCount = (int)((recordCount + PageSize)/PageSize);

    if (recordCount < 1)
    {
        yield break;
    }

    while (page < pageCount)
    {
        var pageData = objectList.Skip(PageSize*page).Take(PageSize).ToList();

        foreach (var rd in pageData)
        {
            yield return rd;
        }
        page++;
    }
}

Чтобы использовать это, у вас будет некоторый запрос linq и передать результат вместе с размером страницы в цикл foreach:

var results = from a in dbContext.Authors
              where a.PublishDate > someDate
              orderby a.Publisher
              select a;

foreach(var author in PagedIterator(results, 100))
{
    // Do Stuff
}

Таким образом, это будет повторяться для каждого автора, выбирая 100 авторов за раз.

person Bitfiddler    schedule 07.12.2012
comment
Поскольку Count () перечисляет коллекцию, вы также можете преобразовать ее в List () и выполнить итерацию с помощью индексов. - person Kaerber; 12.12.2014

РЕДАКТИРОВАТЬ - Удален пропуск (0), поскольку он не нужен

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);
person Jack Marchetti    schedule 04.03.2010
comment
Не следует ли вам изменить порядок методов Take / Skip? Пропуск (0) после Take не имеет смысла. Спасибо за ваш пример в стиле запроса. - person user256890; 04.03.2010
comment
Нет, он прав. Take 10, Skip 0 принимает первые 10 элементов. Skip 0 бессмысленно и никогда не должен выполняться. И порядок Take и Skip имеет значение - Skip 10, Take 10 принимает элементы 10-20; Take 10, Skip 10 не возвращает элементов. - person David Pfeffer; 04.03.2010
comment
Вам также могут понадобиться скобки вокруг запроса перед вызовом Take. (из ... выберите ...) Возьмите (10). Я вызвал конструкцию с выделением строки. Без скобок Take вернул первые 10 символов строки вместо ограничения результата запроса :) - person user256890; 05.03.2010

var pages = items.Select((item, index) => new { item, Page = index / batchSize }).GroupBy(g => g.Page);

Размер партии, очевидно, будет целым числом. Это использует тот факт, что целые числа просто отбрасывают десятичные знаки.

Я наполовину шучу над этим ответом, но он будет делать то, что вы хотите, и, поскольку он отложен, вы не понесете большого ущерба производительности, если сделаете

pages.First(p => p.Key == thePage)

Это решение не для LinqToEntities, я даже не знаю, сможет ли оно превратить это в хороший запрос.

person Todd A. Stedel    schedule 03.07.2015

Подобно ответу Lukazoid, я создал расширение для IQueryable.

   public static IEnumerable<IEnumerable<T>> PageIterator<T>(this IQueryable<T> source, int pageSize)
            {
                Contract.Requires(source != null);
                Contract.Requires(pageSize > 0);
                Contract.Ensures(Contract.Result<IEnumerable<IQueryable<T>>>() != null);

                using (var enumerator = source.GetEnumerator())
                {
                    while (enumerator.MoveNext())
                    {
                        var currentPage = new List<T>(pageSize)
                        {
                            enumerator.Current
                        };

                        while (currentPage.Count < pageSize && enumerator.MoveNext())
                        {
                            currentPage.Add(enumerator.Current);
                        }
                        yield return new ReadOnlyCollection<T>(currentPage);
                    }
                }
            }

Это полезно, если Skip или Take не поддерживаются.

person Michael Freidgeim    schedule 18.03.2018

Я использую такой способ расширения:

public static IQueryable<T> Page<T, TResult>(this IQueryable<T> obj, int page, int pageSize, System.Linq.Expressions.Expression<Func<T, TResult>> keySelector, bool asc, out int rowsCount)
{
    rowsCount = obj.Count();
    int innerRows = rowsCount - (page * pageSize);
    if (innerRows < 0)
    {
        innerRows = 0;
    }
    if (asc)
        return obj.OrderByDescending(keySelector).Take(innerRows).OrderBy(keySelector).Take(pageSize).AsQueryable();
    else
        return obj.OrderBy(keySelector).Take(innerRows).OrderByDescending(keySelector).Take(pageSize).AsQueryable();
}

public IEnumerable<Data> GetAll(int RowIndex, int PageSize, string SortExpression)
{
    int totalRows;
    int pageIndex = RowIndex / PageSize;

    List<Data> data= new List<Data>();
    IEnumerable<Data> dataPage;

    bool asc = !SortExpression.Contains("DESC");
    switch (SortExpression.Split(' ')[0])
    {
        case "ColumnName":
            dataPage = DataContext.Data.Page(pageIndex, PageSize, p => p.ColumnName, asc, out totalRows);
            break;
        default:
            dataPage = DataContext.vwClientDetails1s.Page(pageIndex, PageSize, p => p.IdColumn, asc, out totalRows);
            break;
    }

    foreach (var d in dataPage)
    {
        clients.Add(d);
    }

    return data;
}
public int CountAll()
{
    return DataContext.Data.Count();
}
person Randy    schedule 21.10.2014

    public LightDataTable PagerSelection(int pageNumber, int setsPerPage, Func<LightDataRow, bool> prection = null)
    {
        this.setsPerPage = setsPerPage;
        this.pageNumber = pageNumber > 0 ? pageNumber - 1 : pageNumber;
        if (!ValidatePagerByPageNumber(pageNumber))
            return this;

        var rowList = rows.Cast<LightDataRow>();
        if (prection != null)
            rowList = rows.Where(prection).ToList();

        if (!rowList.Any())
            return new LightDataTable() { TablePrimaryKey = this.tablePrimaryKey };
        //if (rowList.Count() < (pageNumber * setsPerPage))
        //    return new LightDataTable(new LightDataRowCollection(rowList)) { TablePrimaryKey = this.tablePrimaryKey };

        return new LightDataTable(new LightDataRowCollection(rowList.Skip(this.pageNumber * setsPerPage).Take(setsPerPage).ToList())) { TablePrimaryKey = this.tablePrimaryKey };
  }

вот что я сделал. Обычно вы начинаете с 1, но в IList вы начинаете с 0. Так что, если у вас есть 152 строки, это означает, что у вас 8 страниц, но в IList у вас только 7. hop, это может прояснить ситуацию для вас.

person Alen.Toma    schedule 22.10.2015

Есть два основных варианта:

.NET> = 4.0 Динамический LINQ:

  1. Добавить using System.Linq.Dynamic; на вершине.
  2. Используйте 1_

Вы также можете получить его с помощью NuGet.

.NET ‹4.0 Методы расширения:

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);
}
person Jacob    schedule 27.06.2017