On parle beaucoup de Linq pour ce qu’il apporte dans la manipulation des objets issus de bases relationnelles avec Linq to sql, mais il ne faut pas oublier que linq est une grande famille. Pour moi le vrai phénomène se situe sur linq to objects et cette capacité à manipuler simplement des listes d’objets génériques.

Pour faire plaisir à mon ami Fathi qui adore les listes, nous allons voir ici comment implémenter une clause where sur une liste générique afin de pouvoir prendre en compte dynamiquement N paramètres.

Je sais, je me répète mais prenons un exemple simple, c’est encore ce qu’il y a de plus clair. Comme cahier des charges initial, imaginons que je veuille pouvoir effectuer des recherches sur des fichiers:

  • par leur nom avec N mots clés
  • par leur type d’extension, 0..N

 

Vous retrouverez la solution en fin d’article, avec un répertoire contenant les fichiers suivants pour l’exemple (dont certains dans le répertoire subfolder):

city1.txt, city2.txt, city3.doc, user1.txt, user2.txt, user3.txt, user4.xls, user5.xls, subfolder\sub_user (2).txt, subfolder\sub_user (2).xls, subfolder\sub_user (3).txt, subfolder\sub_user (4).txt, subfolder\sub_user (5).txt, subfolder\sub_user.doc, subfolder\sub_user.txt, subfolder\sub_user.xls

Si je dois implémenter ne serait-ce que la recherche d’un mot clé avec une extension de manière classique comme nous l’avons tous déja fait cela devient très vite pénible et nous donnerait quelque chose comme ça:

public static List ClassicSearchInFiles
            (string folder, string keyword, string extension,SearchOption option)
        {
            DirectoryInfo dir = new DirectoryInfo(folder);
            string search = "";
            //if keyword is defined then create the pattern *keyword*
            if (!string.IsNullOrEmpty(keyword))
                search += "*" + keyword + "*";
            //if keyword is not defined add a wildcard for name
            else 
                search += "*";
            
            //if extension is defined then add it (it must be in .txt for exemple)
            if (!string.IsNullOrEmpty(extension))
                search += extension;
            //else add a wildcard
            else
                search += ".*";
            
            return dir.GetFiles(search, option).ToList();
        }

 

Ce n’est pas très glorieux et linq nous permet de faire mieux très rapidement:

public static List GetFilesByTypeAndKeyword(string _folder, string ext, string name)
        {
            DirectoryInfo folder = new DirectoryInfo(_folder);
            List files =
                    (from f in folder.GetFiles("*.*", SearchOption.AllDirectories)
                     where (string.IsNullOrEmpty(ext) || f.Extension == ext) &&
                           (string.IsNullOrEmpty(name) || f.Name.Contains(name))
                     select f).ToList();
            return files;
        }

Nous avons ici de manière plus simple fait la même chose. L’avantage de linq dans ce cas est une écriture plus souple et plus lisible: prendre les fichiers dans la liste de tous les fichiers du répertoire tel que leur extension est égale à ext ou ext est null et que le nom contient le paramètre name ou que celui-ci est vide.

C’est déja beaucoup mieux, mais ce n’est pas encore ce que l’on veut. Nous voudrions avoir plus de souplesse et pouvoir définir un nombre x de paramètres. Pour cela nous allons simple un petit mix de technologies C# 3.

Avant d’aller plus loin nous allons créer une extension pour nous aider à construire les prédicats. Les prédicats correspondent à une expression booléenne que l’on évaluera plus tard (comme n’importe quelle clause where en sql en fait). Cette classe PredicateBuilder est tirée du livre C#3.0 in a Nutshell de Joseph Albahari, dont le site se trouve ici.

public static class PredicateBuilder
    {
        public static Expressionbool>> True() { return f => true; }
        public static Expressionbool>> False() { return f => false; }
 
        public static Expressionbool>> Or(this Expressionbool>> expr1,
                                                            Expressionbool>> expr2)
        {
            var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast());
            return Expression.Lambdabool>>
                  (Expression.Or(expr1.Body, invokedExpr), expr1.Parameters);
        }
 
        public static Expressionbool>> And(this Expressionbool>> expr1,
                                                             Expressionbool>> expr2)
        {
            var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast());
            return Expression.Lambdabool>>
                  (Expression.And(expr1.Body, invokedExpr), expr1.Parameters);
        }
    }

C’est ce prédicat qui va nous permettre de contruire notre ensembles de paramètres. Son principe de fonctionnement est le suivant:

  • J’initialise mon prédicat à true ou false (par les méthodes True ou False de mon PredicateBuilter)
  • j’applique un test booleen et je l’ajoute à mon prédicat (par les opérateurs And ou Or de mon PredicateBuilder)
  • je range le résultat comme nouvelle valeur de mon prédicat
  • je boucle et fait de même pour chaque paramètre
  • j’obtiendrai en sortie une valeur booléenne résultante

 

Soit donc pour tester que Nom vaut A ou B:

  • predicat = true
  • predicat = predicat AND (nom = A ?)
  • predicat = predicat OR (nom = B?)
  • retourne predicat

 

Donc si Nom vaut A ou B le prédicat retourne vrai.

Donc sur ce principe nous allons contruire deux prédicats pour nos deux tests, sur le modèle que je viens de décrire, pour vérifier si le nom des fichiers contient des mots clés et si leur extension fait partie de la liste fournie:

/// 
/// Predicate function to match keyword in Name of files
/// 
/// string list of keywords to search
/// boolean(predicate)
public static Expressionbool>> ContainsInName(
                                            params string[] keywords)
{
    var predicate = PredicateBuilder.False();
    foreach (string keyword in keywords)
    {
        string temp = keyword;
        predicate = predicate.Or(p => p.Name.Contains(temp));
    }
    return predicate;
}

Puis

/// 
/// Predicate function 
/// to match a list of files extensions
/// string list of extensions to find
/// boolean(predicate)
public static Expressionbool>> ExtensionLike(
                                            params string[] keywords)
{
    var predicate = PredicateBuilder.False();
    foreach (string keyword in keywords)
    {
        string temp = keyword;
        predicate = predicate.Or(p => p.Extension == temp);
    }
    return predicate;
}

Puis il ne nous reste qu’à aggréger tout ça dans un seul prédicat et réaliser notre requête:

/// 
/// Extension method to search in File List
/// 
/// DirectoryInfo object
/// the list of file extensions to search for
/// the list of keywords to search for in file names
/// SearchOption enum
/// 
public static List SearchInFiles(
    this DirectoryInfo dir, string[] exts, string[] names, SearchOption recursivity)
{
    var predicate = PredicateBuilder.True();
 
    if (exts.Length > 0)
        predicate = predicate.And(ExtensionLike(exts));
    if (names.Length > 0)
        predicate = predicate.And(ContainsInName(names));
 
    List files = (
            from f in dir.GetFiles("*.*", recursivity)
            select f
            ).AsQueryable()
            .Where(predicate)
            .ToList();
 
    return files;
}

Vous remarquerez plusieurs choses. Tout d’abord c’est plus propre et plus lisible. De plus la méthode est générique et en place, si vous avez besoin de rajouter de paramètres à tester (chercher sur la date de fichier par exemple) il n’y a rajouter un prédicat ;-) . De plus, vous notez le [this] avant le paramètre DirectoryInfo, ce qui veut dire que maintenant sur chaque fois que je vais appeler une classe de ce type, j’aurais directement ma recherche à portée de main:

image

Il n’y a donc plus qu’à utiliser notre méthode pour effectuer notre recherche:

string[] extensions = new string[] { ".txt", ".doc" };
string[] names = new string[] { "3", "2" };
 
DirectoryInfo d = FileSearch.Folder(folder);
 
d.SearchInFiles(extensions, names, SearchOption.AllDirectories)
    .ForEach(
        f => Console.WriteLine(f.FullName.Substring(folder.Length))
        );

Encore une fois, cela n’à peut l’air de rien mais cela permet une plus grande lisibilité et réutilisabilité du code et donc cela laisse plus de temps et de latitude pour se concentrer sur l’architecture et la qualité de son projet!

 

Sources:

 

N’hésitez pas à me poser vos questions ou vos suggestions!

Bons prédicats avec Linq!