Mitsu Furuta a posté sur son blog une question sur C# 3.0 (et il a d'ailleurs l'air chaud-patate pour continuer sur cette lancée):
Â
Le framework 3.5 apporte la méthode AsEnumerable. Quel est donc l'intérêt de ce code étrange !?!?
public static IEnumerable<TSource>
AsEnumerable<TSource>(this IEnumerable<TSource> source)
{
return source;
}
Â
Pour comprendre la réponse, il faut déjà savoir comment fonctionne le compilateur C# face à une expression Linq (Lambda Expression ou écriture simplifiée). Voici un extrait de code :
using System;
using System.Collections.Generic;
using System.Linq;
namespace Quizz
{
public class Person
{
public string Name { get; set; }
public string FirstName { get; set; }
}
public static class Queries
{
public static IQueryable<Person> FilterPersonsByName(IQueryable<Person> personSource
, string pattern)
{
return from p in personSource
where p.Name.Contains(pattern)
select p;
}
public static IEnumerable<Person> FilterPersonsByName(IEnumerable<Person> personSource
, string pattern)
{
return from p in personSource
where p.Name.Contains(pattern)
select p;
}
}
}
Â
Voyons sous Reflector le code véritablement compilé :
Â
public static class Queries
{
// Methods
public static IEnumerable<Person> FilterPersonsByName(IEnumerable<Person> personSource, string pattern)
{
return personSource.Where<Person>(delegate (Person p) {
return p.Name.Contains(pattern);
});
}
public static IQueryable<Person> FilterPersonsByName(IQueryable<Person> personSource, string pattern)
{
ParameterExpression CS$0$0001;
return personSource.Where<Person>(Expression.Lambda<Func<Person, bool>>(Expression.Call(
Expression.Property(CS$0$0001 = Expression.Parameter(typeof(Person), "p")
, (MethodInfo) methodof(Person.get_Name))
, (MethodInfo) methodof(string.Contains)
, new Expression[] { Expression.Constant(pattern) })
, new ParameterExpression[] { CS$0$0001 }));
}
}
Nous n'allons pas détailler, tout ce qu'on remarque, c'est que dans le cas de IEnumerable, le compilateur génère une méthode anonyme, alors que dans le cas d'un IQueryable, il détecte que l'objet est capable d'évaluer un arbre d'expression, et génère donc un arbre d'expression.
La méthode AsEnumerable() permet tout simplement à un IQueryable de se faire passer pour IEnumerable. Voyons un exemple concret :
Â
public static void Main()
{
using (var context = new DataContext())
{
context.Log = Console.Out;
Console.WriteLine("Server Side Paging :");
foreach (var product in (from p in context.Products
where p.ProductName.Contains("a")
select p).Skip(5).Take(5))
{
Console.WriteLine(product.ProductName);
}
Console.WriteLine("\nClient Side Paging :");
foreach (var product in (from p in context.Products
where p.ProductName.Contains("a")
select p).AsEnumerable().Skip(5).Take(5))
{
Console.WriteLine(product.ProductName);
}
}
Console.ReadLine();
}
Dans le premier cas, ".Skip(5).Take(5)" fait partie de l'expression interprétée par Linq To SQL et sera donc traduit en SQL.
Dans le deuxième cas, le compilateur détecte que ".Skip(5).Tage(5)" est appelé sur un objet de type IEnumerable<Product> (et non IQueryable<Product> comme précédement). Du coup, plutôt que d'en faire un arbre d'expression, il en fait un appel de méthode direct. Linq To SQL ne le verra donc pas, et n'en fera pas du code SQL (si vous éxécutez ce code, vous verrez les requètes générées dans la console et comparer le résultat).
L'intérêt de la méthode AsEnumerable() est donc de permettre de stopper l'évaluation de notre requete Linq comme un arbre d'expression. Celà est très utile dans des cas assez variés : requetes cross-databases, expressions non supportées par Linq To SQL, etc.
Un petit exemple :
using (var context = new DataContext())
{
context.Log = Console.Out;
var query = (from cat in context.Categories
let discontinuedProducts = (from p in cat.Products
where p.Discontinued == true
select p)
where discontinuedProducts.Count() > 0
select new { Category = cat
, DiscontinuedProducts = discontinuedProducts })
.Skip(5).Take(5);
foreach (var catWithDisc in query)
{
Console.WriteLine("{0} :", catWithDisc.Category.CategoryName);
foreach (var disc in catWithDisc.DiscontinuedProducts)
Console.WriteLine("\t{0}", disc.ProductName);
}
}
Dans cette exemple, nous effectuons une requetes contenant une sous-requete, et nous faisons de la pagination dessus. Malheureusement, Linq To SQL ne permet pas celà (il nous jette une exception à l'éxécution). Pour palier à ce problème, nous pouvons faire la pagination côté client :
using (var context = new DataContext())
{
context.Log = Console.Out;
var query = (from cat in context.Categories
let discontinuedProducts = (from p in cat.Products
where p.Discontinued == true
select p)
where discontinuedProducts.Count() > 0
select new { Category = cat
, DiscontinuedProducts = discontinuedProducts })
.AsEnumerable().Skip(5).Take(5);
foreach (var catWithDisc in query)
{
Console.WriteLine("{0} :", catWithDisc.Category.CategoryName);
foreach (var disc in catWithDisc.DiscontinuedProducts)
Console.WriteLine("\t{0}", disc.ProductName);
}
}
Au passage, on remarquera le fait la requète SQL renvoit plus de résultats que ce que l'on en traitera côté client. L'impact est minimisé, parce qu'en passant par le pattern IEnumerable, Skip et Take font en sorte que les données qui ne nous intéressent pas ne soient pas conservées dans des objets, côté client.