ChatGPT Image 29 mai 2026, 10_36_09.png

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 :

  • IQueryable

  • Dynamic 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.