Dynamic Linq et IQueryable: Duo gagnant
Simplifier les requêtes dans Umbraco avec Dynamic LINQ et IQueryable
Dans un projet basé sur Umbraco, les besoins de recherche deviennent rapidement plus complexes qu’un simple filtre :
recherche multi-champs
filtrage par tags ou catégories
tri dynamique
pagination
facettes
Très vite, le code LINQ devient répétitif et difficile à maintenir.
Une solution élégante consiste à enrichir IQueryable<T> avec des extensions fluent utilisant System.Linq.Dynamic.Core.
Le résultat : un pipeline de recherche lisible, composable et réutilisable.
Le code source est une série d'extensions utilisable avec IQueryable : DynamicQueryExtensions peut-être utilisé avec Linq aisément
Exemple concret de recherche dans Umbraco
Voici une méthode de recherche complète utilisant des extensions fluent :
public IEnumerable<BlogPost> Search(
IPublishedContent root,
BlogSearchResultModel model,
out int totalItems)
{
totalItems = 0;
if (root == null)
{
return Enumerable.Empty<BlogPost>();
}
var results = root.Descendants<BlogPost>()
.Where(p => p.Content != null && p.Content.Count > 0)
.AsQueryable()
.ApplySearch(
model.Filter.SearchTerm,
nameof(BlogPost.PageTitle),
nameof(BlogPost.Summary))
.ApplyStringArrayFilter(
nameof(BlogPost.Category),
model.BlogSearchFilter.Categories)
.ApplyStringArrayFilter(
nameof(BlogPost.Tags),
model.BlogSearchFilter.Tags)
.ApplyOrdering(
model.Filter.Order,
model.BlogSearchFilter.IsDescending)
.ExecutePaged(
model.BlogSearchFilter.Page - 1,
model.BlogSearchFilter.PageSize);
totalItems = results.RowCount;
return results.Queryable.ToList();
}
Pourquoi cette approche est intéressante?
Le principal avantage est la composition fluide :
query
.ApplySearch(...)
.ApplyStringArrayFilter(...)
.ApplyOrdering(...)
.ExecutePaged(...)
Chaque étape :
reste indépendante
peut être réutilisée ailleurs
ne connaît pas la logique métier globale
Le code devient très lisible et facile à étendre.
Recherche dynamique multi-champs
L’extension ApplySearch() permet de rechercher dynamiquement sur plusieurs propriétés :
public static IQueryable<T> ApplySearch<T>(
this IQueryable<T> query,
string searchString,
params string[] propertyNames)
{
if (string.IsNullOrWhiteSpace(searchString)
|| propertyNames == null
|| propertyNames.Length == 0)
{
return query;
}
var conditions = propertyNames.Select(
p => $"({p} != null && {p}.ToLower().Contains(@0))");
var predicate = string.Join(" OR ", conditions);
return query.Where(
predicate,
searchString.ToLowerInvariant());
}
L’utilisation de nameof() permet d’éviter les “magic strings” :
nameof(BlogPost.PageTitle)
Cela améliore énormément :
le refactoring
l’autocomplétion
la sécurité du code
Filtres sur tableaux (tags, catégories)
Les extensions suivantes permettent de filtrer facilement des collections :
.ApplyStringArrayFilter(
nameof(BlogPost.Tags),
model.BlogSearchFilter.Tags)
La logique interne reste extrêmement simple :
return query.Where(
$"{propertyName}.Any(@0.Contains(it))",
values);
Très pratique pour :
tags
catégories
labels
taxonomies
Tri dynamique
Le tri est lui aussi entièrement dynamique :
.ApplyOrdering(
model.Filter.Order,
model.BlogSearchFilter.IsDescending)
Implémentation :
public static IQueryable<T> ApplyOrdering<T>(
this IQueryable<T> query,
string propertyName,
bool descending)
{
if (string.IsNullOrWhiteSpace(propertyName))
return query;
var ordering = descending
? $"{propertyName} descending"
: propertyName;
return query.OrderBy(ordering);
}
La pagination fluide avec ExecutePaged
Initialement, le principal problème venait du Count() qui cassait le pipeline fluent.
La solution a été d’introduire une extension ExecutePaged() :
public static PagedResult<T> ExecutePaged<T>(
this IQueryable<T> query,
int page,
int pageSize)
{
var total = query.Count();
query = query
.Skip((page - 1) * pageSize)
.Take(pageSize);
return new PagedResult<T>
{
CurrentPage = page,
PageCount = total / pageSize
+ (total % pageSize > 0 ? 1 : 0),
PageSize = pageSize,
RowCount = total,
Queryable = query
};
}
Le pipeline reste alors totalement fluide :
query
.ApplySearch(...)
.ApplyOrdering(...)
.ExecutePaged(...)
Les avantages de cette approche
✔ Très lisible
Le pipeline décrit clairement les étapes de transformation.
✔ Facile à étendre
Ajouter un nouveau filtre devient trivial :
.ApplyDateFilter(...)
.ApplyAuthorFilter(...)
✔ Pragmatique
Pas besoin de :
DDD complexe
expression trees illisibles
architecture “enterprise” lourde
✔ Parfait pour Umbraco
Dans Umbraco, les besoins de recherche sont souvent :
flexibles
orientés contenu
dynamiques
pilotés par l’UI
Cette approche s’intègre donc naturellement avec IPublishedContent.
Les limites
Bien sûr, cette approche a aussi quelques inconvénients.
❌ Dynamic LINQ reste basé sur des strings
Même avec nameof(), les expressions restent interprétées à runtime.
❌ Pas adapté aux très grosses recherches full-text
Pour du vrai moteur de recherche :
scoring
ranking
facettes avancées
fuzzy search
Un moteur comme Lucene reste beaucoup plus adapté.
Conclusion
Cette approche basée sur :
IQueryableDynamic LINQ
extensions fluent
nameof()
offre un excellent compromis entre :
simplicité
flexibilité
lisibilité
maintenabilité
Sans tomber dans une architecture surcomplexifiée.
Dans beaucoup de projets Umbraco, cela suffit largement avant de devoir introduire une couche de recherche plus avancée basée sur Lucene ou Examine.