Paginación con LINQ para objetos

90

¿Cómo implementaría la paginación en una consulta LINQ? En realidad, por el momento, estaría satisfecho si se pudiera imitar la función sql TOP. Sin embargo, estoy seguro de que la necesidad de un soporte completo de paginación surge más pronto después de todos modos.

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

Respuestas:

231

Estás buscando los métodos de extensión Skipy Take. Skippasa los primeros N elementos en el resultado, devolviendo el resto; Takedevuelve los primeros N elementos en el resultado, descartando los elementos restantes.

Consulte MSDN para obtener más información sobre cómo utilizar estos métodos: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Suponiendo que ya está teniendo en cuenta que pageNumber debe comenzar en 0 (disminuya por 1 como se sugiere en los comentarios), podría hacerlo así:

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

De lo contrario, como sugiere @Alvin

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * (pageNumber - 1))
  .Take(numberOfObjectsPerPage);
David Pfeffer
fuente
7
¿Debo usar la misma técnica sobre SQL con una base de datos enorme, primero llevará toda la tabla a la memoria y luego desechará lo no deseado?
user256890
1
Si está interesado en lo que sucede bajo el capó, por cierto, la mayoría de los controladores de bases de datos LINQ proporcionan una forma de obtener información de salida de depuración para el SQL real que se está ejecutando.
David Pfeffer
Rob Conery escribió en su blog sobre una clase PagedList <T> que puede ayudarlo a comenzar. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello
49
esto resultará en saltarse la primera página SI pageNumber no está basado en cero (0). si pageNumber comienza con 1, por lo tanto, use este ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin
¿Cómo será el SQL resultante, el que llegará a la base de datos?
Faiz
53

Usar Skipy Takees definitivamente el camino a seguir. Si estuviera implementando esto, probablemente escribiría mi propio método de extensión para manejar la paginación (para hacer el código más legible). Por supuesto, la implementación puede utilizar Skipy 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);
  }
}

La clase define dos métodos de extensión: uno para IEnumerabley otro para IQueryable, lo que significa que puede usarlo tanto con LINQ to Objects como con LINQ to SQL (al escribir una consulta de base de datos, el compilador elegirá la IQueryableversión).

Dependiendo de sus requisitos de paginación, también podría agregar algún comportamiento adicional (por ejemplo, para manejar negativo pageSizeo pagevalor). Aquí hay un ejemplo de cómo usaría este método de extensión en su consulta:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);
Tomas Petricek
fuente
3
Creo que esto devolverá el conjunto de resultados completo y luego filtrará en la memoria en lugar de en el servidor. Gran impacto en el rendimiento de una base de datos si se trata de SQL.
jvenema
1
@jvenema Tienes razón. Dado que se utiliza la IEnumerableinterfaz en lugar de IQueryableesto, se extraerá toda la tabla de la base de datos, lo que supondrá un gran impacto en el rendimiento.
David Pfeffer
2
Por supuesto, puede agregar fácilmente una sobrecarga para IQueryableque funcione también con consultas de base de datos (edité la respuesta y la agregué). Es un poco desafortunado que no pueda escribir el código de una manera completamente genérica (en Haskell esto sería posible con clases de tipos). La pregunta original mencionaba LINQ to Objects, así que escribí solo una sobrecarga.
Tomas Petricek
Estaba pensando en implementar esto yo mismo. Estoy un poco sorprendido de que no sea parte de la implementación estándar. ¡Gracias por el código de muestra!
Michael Richardson
1
Creo que el ejemplo debería ser: public static IQueryable <T> Page <T> (... etc
David Talbot
37

Aquí está mi enfoque de rendimiento para la paginación cuando uso LINQ to objects:

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

Esto luego se puede usar así:

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 de esta basura Skipy Takeque será muy ineficaz si estás interesado en varias páginas.

Lukazoide
fuente
1
Funciona en Entity Framework con Azure SQL Data Warehouse, que no admite el método Skip (internamente con la cláusula OFFSET)
Michael Freidgeim
4
Esto solo tenía que ser robado y ponerlo en mi biblioteca común, ¡gracias! Acabo de renombrar el método Paginatepara eliminar nounvs verbambigüedad.
Gabrielius
9
   ( for o in objects
    where ...
    select new
   {
     A=o.a,
     B=o.b
   })
.Skip((page-1)*pageSize)
.Take(pageSize)
Navidad
fuente
6

No sé si esto ayudará a alguien, pero lo encontré útil para mis propósitos:

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 esto, tendría una consulta de linq y pasaría el resultado junto con el tamaño de la página a un bucle 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
}

Entonces, esto iterará sobre cada autor obteniendo 100 autores a la vez.

Bitfiddler
fuente
Como Count () enumera la colección, también puede convertirla a List () e iterar con índices.
Kaerber
5

EDITAR: se eliminó Skip (0) ya que no es necesario

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);
Jack Marchetti
fuente
2
¿No debería cambiar el orden de los métodos Take / Skip? Saltar (0) después de Tomar no tiene sentido. Gracias por dar su ejemplo en estilo de consulta.
user256890
2
No, tiene razón. Take10, Skip0 toma los primeros 10 elementos. Skip0 no tiene sentido y nunca debería hacerse. Y el orden de Takey Skipimporta - Skip10, Take10 toma elementos 10-20; Take10, Skip10 no devuelve ningún elemento.
David Pfeffer
Es posible que también necesite corchetes alrededor de la consulta antes de llamar a Take. (de ... seleccione ...). Tome (10). Llamé a la construcción con la selección de una cadena. Sin corchetes, Take devolvió los primeros 10 caracteres de la cadena en lugar de limitar el resultado de la consulta :)
user256890
3
var pages = items.Select((item, index) => new { item, Page = index / batchSize }).GroupBy(g => g.Page);

Obviamente, el tamaño del lote será un número entero. Esto aprovecha el hecho de que los números enteros simplemente caen decimales.

Estoy medio bromeando con esta respuesta, pero hará lo que quieras y, como está diferida, no incurrirás en una gran penalización de rendimiento si lo haces.

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

Esta solución no es para LinqToEntities, ni siquiera sé si podría convertir esto en una buena consulta.

Todd A. Stedel
fuente
3

Similar a la respuesta de Lukazoid, he creado una extensión 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);
                    }
                }
            }

Es útil si no se admiten Saltar o Tomar.

Michael Freidgeim
fuente
1

Yo uso este método de extensión:

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

Esto es lo que hice. Normalmente comienzas en 1, pero en IList comienzas con 0. así que si tienes 152 filas, eso significa que tienes 8 paginación, pero en IList solo tienes 7. hop, esto puede dejarte claro.

Alen.Toma
fuente
1

var results = (medicineInfo.OrderBy(x=>x.id)
                       .Skip((pages -1) * 2)
                       .Take(2));

Debendra Dash
fuente
1

Hay dos opciones principales:

.NET> = 4.0 LINQ dinámico :

  1. Agregar usando System.Linq.Dynamic; en la cima.
  2. Utilizar: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

También puede obtenerlo mediante NuGet .

Métodos de extensión .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);
}
Jacob
fuente