Pagination avec LINQ pour les objets

90

Comment implémenteriez-vous la pagination dans une requête LINQ? En fait, pour le moment, je serais satisfait si la fonction SQL TOP pouvait être imitée. Cependant, je suis sûr que le besoin d'une prise en charge complète de la pagination apparaîtra plus tôt de toute façon.

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

Réponses:

231

Vous recherchez les méthodes d'extension Skipet Take. Skippasse au-delà des N premiers éléments du résultat, renvoyant le reste; Takerenvoie les N premiers éléments du résultat, en supprimant tous les éléments restants.

Consultez MSDN pour plus d'informations sur l'utilisation de ces méthodes: http://msdn.microsoft.com/en-us/library/bb386988.aspx

En supposant que vous tenez déjà compte que le numéro de page doit commencer à 0 (diminution par 1 comme suggéré dans les commentaires) Vous pouvez le faire comme ceci:

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

Sinon comme suggéré par @Alvin

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * (pageNumber - 1))
  .Take(numberOfObjectsPerPage);
David Pfeffer
la source
7
Dois-je utiliser la même technique sur SQL avec une énorme base de données, est-ce que cela prendra d'abord la table entière en mémoire, puis rejettera les indésirables?
user256890
1
Si vous êtes intéressé par ce qui se passe sous le capot, en passant, la plupart des pilotes de base de données LINQ fournissent un moyen d'obtenir des informations de sortie de débogage pour le SQL réel en cours d'exécution.
David Pfeffer
Rob Conery a publié un blog sur une classe PagedList <T> qui peut vous aider à démarrer. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello
49
cela entraînera le saut de la première page SI pageNumber n'est pas basé sur zéro (0). si pageNumber commence par 1, utilisez donc ce ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin
À quoi ressemblera le SQL résultant, celui qui atteindra la base de données?
Faiz
53

Utiliser Skipet Takeest définitivement la voie à suivre. Si je mettais en œuvre cela, j'écrirais probablement ma propre méthode d'extension pour gérer la pagination (pour rendre le code plus lisible). La mise en œuvre peut bien sûr utiliser Skipet 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 classe définit deux méthodes d'extension - une pour IEnumerableet une pour IQueryable, ce qui signifie que vous pouvez l'utiliser avec LINQ to Objects et LINQ to SQL (lors de l'écriture de la requête de base de données, le compilateur choisira la IQueryableversion).

En fonction de vos besoins de pagination, vous pouvez également ajouter un comportement supplémentaire (par exemple pour gérer une valeur négative pageSizeou une pagevaleur). Voici un exemple d'utilisation de cette méthode d'extension dans votre requête:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);
Tomas Petricek
la source
3
Je pense que cela renverra l'ensemble des résultats, puis filtrera en mémoire au lieu de sur le serveur. Énorme performance contre une base de données s'il s'agit de SQL.
jvenema
1
@jvenema Vous avez raison. Étant donné que cela utilise l' IEnumerableinterface plutôt que IQueryablecela, la table de base de données entière sera extraite, ce qui sera un impact majeur sur les performances.
David Pfeffer
2
Vous pouvez bien sûr facilement ajouter une surcharge pour IQueryablequ'elle fonctionne également avec les requêtes de base de données (j'ai modifié la réponse et l'ai ajoutée). Il est un peu dommage que vous ne puissiez pas écrire le code de manière totalement générique (en Haskell, cela serait possible avec des classes de types). La question originale mentionnait LINQ to Objects, je n'ai donc écrit qu'une seule surcharge.
Tomas Petricek
Je pensais juste à mettre cela en œuvre moi-même. Je suis un peu surpris que cela ne fasse pas partie de l'implémentation standard. Merci pour l'exemple de code!
Michael Richardson
1
Je pense que l'exemple devrait être: public static IQueryable <T> Page <T> (... etc
David Talbot
37

Voici mon approche performante de la pagination lors de l'utilisation de LINQ vers des objets:

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

Cela peut ensuite être utilisé comme ceci:

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

Rien de tout ça Skipet Takequi sera très inefficace si vous êtes intéressé par plusieurs pages.

Lukazoïde
la source
1
Cela fonctionne dans Entity Framework avec Azure SQL Data Warehouse, qui ne prend pas en charge la méthode Skip (en utilisant en interne la clause OFFSET)
Michael Freidgeim
4
Cela devait juste être volé et mis dans ma bibliothèque commune, merci! Je viens de renommer la méthode Paginatepour supprimer nouncontre l' verbambiguïté.
Gabrielius
9
   ( for o in objects
    where ...
    select new
   {
     A=o.a,
     B=o.b
   })
.Skip((page-1)*pageSize)
.Take(pageSize)
Noel
la source
6

Je ne sais pas si cela aidera quelqu'un, mais je l'ai trouvé utile pour mes besoins:

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

Pour utiliser cela, vous auriez une requête linq et passer le résultat avec la taille de la page dans une boucle 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
}

Cela va donc itérer sur chaque auteur en récupérant 100 auteurs à la fois.

Bitfiddler
la source
Comme Count () énumère la collection, vous pouvez tout aussi bien la convertir en List () et itérer avec des index.
Kaerber
5

EDIT - Suppression du saut (0) car ce n'est pas nécessaire

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);
Jack Marchetti
la source
2
Ne devriez-vous pas changer l'ordre des méthodes Take / Skip? Passer (0) après Take n'a pas de sens. Merci pour votre exemple dans le style de requête.
user256890
2
Non, il a raison. Take10, Skip0 prend les 10 premiers éléments. Skip0 est inutile et ne devrait jamais être fait. Et l'ordre de Takeet Skipcompte - Skip10, Take10 prend les éléments 10-20; Take10, Skip10 ne renvoie aucun élément.
David Pfeffer
Vous pouvez également avoir besoin de crochets autour de la requête avant d'appeler Take. (à partir de ... sélectionnez ...) Prenez (10). J'ai appelé la construction en sélectionnant une chaîne. Sans crochets, la prise a renvoyé les 10 premiers caractères de la chaîne au lieu de limiter le résultat de la requête :)
user256890
3
var pages = items.Select((item, index) => new { item, Page = index / batchSize }).GroupBy(g => g.Page);

Batchsize sera évidemment un entier. Cela tire parti du fait que les nombres entiers suppriment simplement les décimales.

Je plaisante à moitié avec cette réponse, mais elle fera ce que vous voulez, et comme elle est différée, vous n'encourrez pas une grosse pénalité de performance si vous le faites

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

Cette solution n'est pas pour LinqToEntities, je ne sais même pas si cela pourrait en faire une bonne requête.

Todd A. Stedel
la source
3

Semblable à la réponse de Lukazoid, j'ai créé une extension pour 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);
                    }
                }
            }

C'est utile si Skip ou Take ne sont pas pris en charge.

Michael Freidgeim
la source
1

J'utilise cette méthode d'extension:

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();
}
Excité
la source
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 };
  }

c'est ce que j'ai fait. Normalement, vous commencez à 1 mais dans IList vous commencez par 0. Donc, si vous avez 152 lignes, cela signifie que vous avez 8 pagination, mais dans IList, vous n'avez que 7. hop, cela peut clarifier les choses pour vous

Alen.Toma
la source
1

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

Debendra Dash
la source
1

Il existe deux options principales:

.NET> = 4.0 LINQ dynamique :

  1. Ajouter à l'aide de System.Linq.Dynamic; au sommet.
  2. Utilisation: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Vous pouvez également l'obtenir par NuGet .

Méthodes d'extension .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
la source