Paginação com LINQ para objetos


90

Como você implementaria a paginação em uma consulta LINQ? Na verdade, por enquanto, eu ficaria satisfeito se a função sql TOP pudesse ser imitada. No entanto, tenho certeza de que a necessidade de suporte de paginação completo surgirá mais tarde, de qualquer maneira.

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

Respostas:


231

Você está procurando os métodos de extensão Skipe Take. Skippassa pelos primeiros N elementos no resultado, retornando o restante; Takeretorna os primeiros N elementos no resultado, eliminando todos os elementos restantes.

Consulte MSDN para obter mais informações sobre como usar esses métodos: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Supondo que você já esteja levando em consideração que o pageNumber deve começar em 0 (diminuir em 1 conforme sugerido nos comentários). Você poderia fazer assim:

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

Caso contrário, conforme sugerido por @Alvin

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

7
Devo usar a mesma técnica em SQL com um banco de dados enorme, isso levará a tabela inteira para a memória primeiro e, em seguida, jogará fora o indesejado?
user256890

1
Se você estiver interessado no que está acontecendo nos bastidores, a propósito, a maioria dos drivers de banco de dados LINQ fornecem uma maneira de obter informações de saída de depuração para o SQL real que está sendo executado.
David Pfeffer

Rob Conery escreveu um blog sobre uma classe PagedList <T> que pode ajudá-lo a começar. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
isso resultará em pular a primeira página SE o pageNumber não for baseado em zero (0). se pageNumber começar com 1, use este ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin

Como será o SQL resultante, aquele que atinge o banco de dados?
Faiz

53

Usar Skipe Takeé definitivamente o caminho a percorrer. Se eu estivesse implementando isso, provavelmente escreveria meu próprio método de extensão para lidar com a paginação (para tornar o código mais legível). A implementação pode, obviamente, usar Skipe 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);
  }
}

A classe define dois métodos de extensão - um para IEnumerablee um para IQueryable, o que significa que você pode usá-lo com LINQ to Objects e LINQ to SQL (ao escrever uma consulta de banco de dados, o compilador escolherá a IQueryableversão).

Dependendo dos seus requisitos de paginação, você também pode adicionar algum comportamento adicional (por exemplo, para lidar com valores negativos pageSizeou page) Aqui está um exemplo de como você usaria este método de extensão em sua consulta:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);

3
Acredito que isso retornará todo o conjunto de resultados e, em seguida, filtrará na memória em vez de no servidor. Grande impacto de desempenho em um banco de dados se for SQL.
jvenema

1
@jvenema Você está certo. Como isso está usando a IEnumerableinterface, em vez de IQueryablepuxar toda a tabela do banco de dados, será um grande impacto no desempenho.
David Pfeffer

2
É claro que você pode adicionar facilmente uma sobrecarga para IQueryablefazê-lo funcionar com consultas de banco de dados também (eu editei a resposta e a adicionei). É um pouco lamentável que você não possa escrever o código de uma forma totalmente genérica (em Haskell isso seria possível com classes de tipo). A pergunta original mencionava LINQ to Objects, então escrevi apenas uma sobrecarga.
Tomas Petricek

Eu estava pensando em implementar isso sozinho. Estou um pouco surpreso por não fazer parte da implementação padrão. Obrigado pelo código de amostra!
Michael Richardson

1
Acho que o exemplo deveria ser: public static IQueryable <T> Page <T> (... etc
David Talbot

37

Esta é minha abordagem de desempenho para paginação ao usar LINQ para objetos:

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);
        }
    }
}

Isso pode ser usado assim:

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       
    }
}

Nada disso é lixo Skipe Takeque será altamente ineficiente se você estiver interessado em várias páginas.


1
Ele funciona no Entity Framework com Azure SQL Data Warehouse, que não oferece suporte ao método Skip (internamente usando a cláusula OFFSET)
Michael Freidgeim

4
Isso só tinha que ser roubado e colocado na minha biblioteca comum, obrigado! Eu só renomeado o método para Paginateremover nounvs verbambigüidade.
Gabrielius

9
   ( for o in objects
    where ...
    select new
   {
     A=o.a,
     B=o.b
   })
.Skip((page-1)*pageSize)
.Take(pageSize)

6

Não sei se isso vai ajudar alguém, mas achei útil para meus objetivos:

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++;
    }
}

Para usar isso, você deve ter alguma consulta linq e passar o resultado junto com o tamanho da página em um loop 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
}

Portanto, isso irá iterar sobre cada autor, obtendo 100 autores de cada vez.


Como Count () enumera a coleção, você também pode convertê-la em List () e iterar com índices.
Kaerber

5

EDIT - Removido Skip (0) porque não é necessário

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);

2
Você não deveria alterar a ordem dos métodos Take / Skip? Pular (0) depois de tirar não faz sentido. Obrigado por dar o seu exemplo no estilo de consulta.
user256890

2
Não, ele está certo. Take10, Skip0 leva os primeiros 10 elementos. Skip0 é inútil e nunca deve ser feito. E a ordem de Takee Skipimporta - Skip10, Take10 leva os elementos 10-20; Take10, Skip10 não retorna nenhum elemento.
David Pfeffer

Você também pode precisar de colchetes ao redor da consulta antes de chamar Take. (de ... selecione ...) Pegue (10). Eu chamei a construção com a seleção de uma string. Sem colchetes, o Take retornou os primeiros 10 caracteres da string em vez de limitar o resultado da consulta :)
user256890

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

Batchsize obviamente será um número inteiro. Isso tira vantagem do fato de que os inteiros simplesmente perdem as casas decimais.

Estou meio que brincando com esta resposta, mas ela fará o que você quiser e, como é adiada, você não incorrerá em uma grande penalidade de desempenho se o fizer

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

Essa solução não é para LinqToEntities, nem sei se poderia transformar isso em uma boa consulta.


3

Semelhante à resposta de Lukazoid, criei uma extensão para 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);
                    }
                }
            }

É útil se Skip ou Take não forem suportados.


1

Eu uso este método de extensão:

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();
}

1
    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 };
  }

isso é o que eu fiz. Normalmente você começa em 1, mas em IList você começa com 0. então se você tiver 152 linhas, isso significa que você tem 8 paginação, mas em IList você só tem 7. hop, isso pode deixar as coisas claras para você



1

Existem duas opções principais:

.NET> = 4.0 LINQ dinâmico :

  1. Adicione usando System.Linq.Dynamic; no topo.
  2. Usar: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Você também pode obtê-lo pelo NuGet .

Métodos de extensão .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);
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.