INTRODUCTION · LINQeQuantic.LinqeQuantic.Linq construit la requête comme un objet : des filtres, un tri et des spécifications qui se combinent, circulent entre les couches et se testent sans base de données.
Aperçu
Il répond au problème qu'une requête dynamique crée d'ordinaire — un contrôleur qui connaît la forme des données — en laissant la couche de présentation passer un filtre à la couche de service, et non une clause.Trois implémentations s'accordent : C# avec générateurs de code, TypeScript pour le navigateur, et un système de conversion typé entre DTOs et entités.
Installer
shell
dotnet add package eQuantic.Linq
Ce qu'elle fait
Générateurs de codeLes spécifications, les filtres et les trieurs sont écrits à la compilation, sur .NET 6 et suivants, si bien que rien n'est résolu par réflexion pendant que la requête attend.
Le modèle SpecificationLe modèle entier, avec AND et OR, pour qu'une règle métier s'écrive une fois et se combine ensuite, au lieu d'être recopiée dans la requête suivante.
Filtre et trieur d'entitésLe filtrage et le tri circulent de la couche de présentation vers celle de service sous forme de valeurs, si bien qu'aucune couche n'a à savoir ce que l'autre interroge.
Conversion à types conservésTransformation entre DTOs et entités, configurée de façon fluide et vérifiée à la compilation plutôt qu'à l'exécution.
Expressions en cacheLes expressions lambda sont mises en cache, si bien qu'une requête exécutée dans une boucle est compilée une fois et non à chaque tour.
Asynchrone de bout en boutDes API asynchrones pour le filtre et le trieur d'entités, jeton d'annulation compris.
La même chose en TypeScriptUne implémentation parallèle pour le navigateur, génériques et vérification des types conservés, si bien qu'un filtre veut dire la même chose des deux côtés.
Une API de requêtes fluideIQuery, IValueQuery et IComparisonQuery, pour bâtir une requête étape par étape et l'exécuter une fois terminée.
Générateurs de codeLes spécifications, les filtres et les trieurs sont écrits à la compilation, sur .NET 6 et suivants, si bien que rien n'est résolu par réflexion pendant que la requête attend.
Le modèle SpecificationLe modèle entier, avec AND et OR, pour qu'une règle métier s'écrive une fois et se combine ensuite, au lieu d'être recopiée dans la requête suivante.
Filtre et trieur d'entitésLe filtrage et le tri circulent de la couche de présentation vers celle de service sous forme de valeurs, si bien qu'aucune couche n'a à savoir ce que l'autre interroge.
Conversion à types conservésTransformation entre DTOs et entités, configurée de façon fluide et vérifiée à la compilation plutôt qu'à l'exécution.
Expressions en cacheLes expressions lambda sont mises en cache, si bien qu'une requête exécutée dans une boucle est compilée une fois et non à chaque tour.
Asynchrone de bout en boutDes API asynchrones pour le filtre et le trieur d'entités, jeton d'annulation compris.
La même chose en TypeScriptUne implémentation parallèle pour le navigateur, génériques et vérification des types conservés, si bien qu'un filtre veut dire la même chose des deux côtés.
Une API de requêtes fluideIQuery, IValueQuery et IComparisonQuery, pour bâtir une requête étape par étape et l'exécuter une fois terminée.
Démarrage rapide
Installer les deux paquets — La bibliothèque, et le générateur à côté. Le générateur est une dépendance de compilation, il n'est donc livré avec rien.
shell
dotnet add package eQuantic.Linqdotnet add package eQuantic.Linq.SourceGenerator
Marquer le modèle — Un attribut sur la classe, et un par propriété disant quels opérateurs cette propriété doit recevoir.
C#
builder.Services.AddLinqServices(); [GenerateSpecifications]public class Product{ public int Id { get; set; } [SpecProperty(FilterOperators = FilterOperator.Equals | FilterOperator.Contains)] public string Name { get; set; } = string.Empty; [SpecProperty(FilterOperators = FilterOperator.GreaterThan | FilterOperator.LessThan)] public decimal Price { get; set; } public bool IsActive { get; set; }}
Utiliser ce que le générateur a écrit — Les spécifications et les filtres arrivent comme de vrais membres : ils s'autocomplètent, et une coquille est une erreur de compilation.
C#
public partial class Product{ public static class Specifications { public static ISpecification<Product> NameEquals(string value) => new DirectSpecification<Product>(p => p.Name == value); public static ISpecification<Product> NameContains(string value) => new DirectSpecification<Product>(p => p.Name.Contains(value)); public static ISpecification<Product> PriceGreaterThan(decimal value) => new DirectSpecification<Product>(p => p.Price > value); } public static class Filters { public static IEntityFilter<Product> ByName(string name) => EntityFilter<Product>.Where(NameEquals(name)); public static IEntityFilter<Product> ByPriceRange(decimal min, decimal max) => EntityFilter<Product>.Where(PriceGreaterThan(min).And(p => p.Price <= max)); }}
Passer le filtre entre les couches — Le service reçoit un filtre et ne voit jamais de chaîne de requête ; le point d'entrée construit le filtre et ne voit jamais la base de données.
C#
public class ProductService{ private readonly IRepository<Product> _repository; public async Task<IEnumerable<Product>> GetProductsAsync(IEntityFilter<Product> filter) { return await _repository.GetAsync(filter.Specification); }} app.MapGet("/products", async ( [AsParameters] ProductSearchRequest request, ProductService products) =>{ var filter = EntityFilter<Product>.Empty(); if (!string.IsNullOrEmpty(request.Name)) filter = filter.And(Product.Specifications.NameContains(request.Name)); if (request.MinPrice.HasValue) filter = filter.And(Product.Specifications.PriceGreaterThan(request.MinPrice.Value)); return Results.Ok(await products.GetProductsAsync(filter));});
Convertir entre modèles — Mappez une propriété imbriquée, renommez-en une, et refusez de compiler si quelque chose est resté sans correspondance.
C#
public class ProductDto{ public string Name { get; set; } = string.Empty; public string Category { get; set; } = string.Empty; public bool IsAvailable { get; set; }} var products = await _repository.GetAsync<Product>() .Cast<ProductDto>(options => options .Map(src => src.Name, dest => dest.Name) .Map(src => src.Category.Name, dest => dest.Category) .Map(src => src.IsActive, dest => dest.IsAvailable) .ExcludeUnmapped()); public class ProductCastConfiguration : IFilteringCastConfiguration<ProductDto>{ public void Configure(FilteringCastOptions<ProductDto> options) { options.Map(src => src.Name, dest => dest.Name) .Map(src => src.IsActive, dest => dest.IsAvailable); }}
La même API dans le navigateur — Le côté TypeScript construit les mêmes filtres et les sérialise dans la chaîne attendue par l'API.
TypeScript
import { Filtering, FilteringCollection, Sorting, SortingCollection } from 'equantic-linq'; interface User { id: number; name: string; email: string; age: number; isActive: boolean;} const filters = new FilteringCollection<User>([ Filtering.ct<User, 'name'>('name', 'John'), Filtering.gte<User, 'age'>('age', 25), Filtering.eq<User, 'isActive'>('isActive', true),]); const sorts = new SortingCollection<User>([ Sorting.desc<User>('age'), Sorting.asc<User>('name'),]); filters.toString(); // name:ct(John),age:gte(25),isActive:eq(true)sorts.toString(); // age:desc,name:asc
Bâtir une requête étape par étape — L'API fluide pour les cas où la requête s'assemble dans le code : conditions, ordre, pagination, et une agrégation à la fin.
C#
var query = Query.For<Product>() .Where(p => p.IsActive) .OrderBy(p => p.Name) .Skip(10) .Take(20); var totalPrice = await ValueQuery.For<Product>() .Where(p => p.IsActive) .Sum(p => p.Price) .ExecuteAsync(); var hasExpensiveProducts = await ComparisonQuery.For<Product>() .Any(p => p.Price > 1000) .ExecuteAsync(); var complexQuery = Query.For<Product>() .Where(p => p.IsActive) .And(p => p.Price > 100) .Or(p => p.Category == "Premium") .OrderBy(p => p.Price) .ThenBy(p => p.Name);
Surface publique
ESPACE DE NOMS
SURFACE PUBLIQUE
GenerateSpecificationsAttribute
Marque une classe pour laquelle le générateur doit écrire des spécifications.
SpecPropertyAttribute
Indique quels opérateurs de filtre une propriété doit recevoir.
ISpecification<T>
Le contrat de spécification, au-dessus d'une expression lambda.
IEntityFilter<T>
Le filtre qu'une couche remet à une autre.
IEntitySorter<T>
Le même, pour le tri, avec une surface fluide.
IAsyncEntityFilter<T>
Le filtre asynchrone, avec jeton d'annulation.
Filtering<TData, TColumn>
Filtrage sur une colonne, propriétés imbriquées comprises.
FilterOperator
Les opérateurs qu'un filtre peut employer : égal, contient, commence par, supérieur à, et les autres.
IExpressionCache
Là où une expression lambda compilée est conservée entre les appels.
Query<T>
Le point d'entrée de l'API de requêtes fluide.
Vous préférez lire du code ?
Chaque paquet est sous licence MIT et public, et les exemples sont prêts à copier-coller.
Prêt à livrer quelque chose de réel ?
Réservez un appel de découverte de 30 minutes. Dans l'heure, nous vous dirons si nous sommes le bon partenaire — ou qui l'est.
Prêt à livrer quelque chose de réel ?
Réservez un appel de découverte de 30 minutes. Dans l'heure, nous vous dirons si nous sommes le bon partenaire — ou qui l'est.
© 2026 eQuantic Tech. Tous droits réservés.