Lyo.Query
3.0.0
dotnet add package Lyo.Query --version 3.0.0
NuGet\Install-Package Lyo.Query -Version 3.0.0
<PackageReference Include="Lyo.Query" Version="3.0.0" />
<PackageVersion Include="Lyo.Query" Version="3.0.0" />
<PackageReference Include="Lyo.Query" />
paket add Lyo.Query --version 3.0.0
#r "nuget: Lyo.Query, 3.0.0"
#:package Lyo.Query@3.0.0
#addin nuget:?package=Lyo.Query&version=3.0.0
#tool nuget:?package=Lyo.Query&version=3.0.0
Lyo.Query
Execution engine for Lyo.Query.Models filter trees. IWhereClauseService / BaseWhereClauseService turns polymorphic WhereClause ASTs
(condition / group, optional SubClause) into LINQ expression trees on IQueryable<T>, applies multi-key SortBy, and can evaluate / explain the same AST against a loaded
entity in memory.
What this package is. AST → expression / matcher. What it is not. HTTP endpoints, QueryConcreteReq / ProjectionQueryReq / QueryReq builders, or query- result
caching. Those live in Models + Lyo.Api.
EF-agnostic at the boundary (works on any IQueryable, including EF DbSet and in-memory). Needs ICacheService + CacheOptions for compiled predicate / matcher / metadata
caches.
Targets net10.0.
Features
- AST →
IQueryable.ApplyWhereClausebuilds expression trees fromWhereClausethat EF (or the provider) can translate. - 18 comparators. Equals / NotEquals / Contains* / StartsWith* / EndsWith* / Greater* / Less* / In / NotIn / Regex / NotRegex / Fuzzy / NotFuzzy.
- Collection paths.
Lines.QuantitybecomesAny(...)on collection elements. Bare collection /.Countstyle paths compare against count. - Two-phase SubClause.
includeSubClauses: falsefor the SQL root pass. Load includes then match the sub-tree in memory. - Multi-key sort.
SortByProperty+ApplyOrderingusingPriorityand a stable default tie-break. - In-memory match + explain.
MatchesWhereClause/ExplainMatchwith blocking path and OR-branch detail. UseExplainMatch(...).ToErrors()for structured validation errors. - ICache hot path. Compiled EF predicates (
filter_ef_predicate:*), matchers (filter_matcher:*), sort keys, and property-path metadata. - Path validation. Bad dotted paths raise
InvalidQueryExceptionviaTryValidatePropertyPath. - Contains→Regex coalesce. Adjacent
Containsleaves on the same string field can merge into a singleRegexalternation. - Property diffing.
IPropertyComparisonServicefor patch and update pipelines. - Value coercion.
IValueConversionServicemaps JSON literals → CLR property types (suppressible whenLyo.Apiowns conversion). - QueryProject aggregates.
AggregateQuerygroups anIQueryablebySelectand projectsCount,CountDistinct,Sum,Average,Min, andMax. EmptySelectis one summary row.Countwith no field counts rows in the group. A collection field, or a path ending in.count, totals that collection. Select and aggregate paths accept[n],[start:],[:end],[start:end], and[:]on a string or a collection.
Examples
Add query services
using Lyo.Query;
services.AddLocalCache(/* … */); // or AddFusionCache(...)
services.AddLyoQueryServices();
// registers IValueConversionService, IPropertyComparisonService, IWhereClauseService (singletons)
// Hosting Lyo.Api (richer ITypeConversionService already registered):
// services.AddLyoQueryServices(registerValueConversion: false);
Filter an IQueryable
using Lyo.Query;
using Lyo.Query.Models;
using Lyo.Query.Models.Builders;
var where = WhereClauseBuilder.And()
.Equals("Status", "Active")
.GreaterThan("Age", 18)
.Build();
IQueryable<Person> people = db.People.AsQueryable();
var filtered = whereClauseService.ApplyWhereClause(people, where);
var page = await filtered.Skip(20).Take(20).ToListAsync(ct);
SQL first, then in-memory SubClause
// Root runs as SQL; SubClause refined after Include load
var where = WhereClauseBuilder.And()
.Equals("Status", "Open")
.AddSubClause(sub => sub.Contains("Tags", "vip"))
.Build();
var sqlPhase = whereClauseService.ApplyWhereClause(db.Orders, where, includeSubClauses: false);
var includePaths = whereClauseService.GetCollectionIncludePathsForWhereClause<Order>(where);
// apply Include(includePaths…) then materialize
var rows = await sqlPhase /* .Include(...) */ .ToListAsync(ct);
var matched = rows.Where(o => whereClauseService.MatchesWhereClause(o, where)).ToList();
Order by several keys
using Lyo.Query.Models;
using Lyo.Common.Core.Enums;
var sorted = whereClauseService.ApplyOrdering(
queryable: people,
sortByProps:
[
new SortBy { PropertyName = "LastName", Direction = SortDirection.Ascending, Priority = 0 },
new SortBy { PropertyName = "FirstName", Direction = SortDirection.Ascending, Priority = 1 },
],
defaultOrder: p => p.Id,
defaultSortDirection: SortDirection.Ascending);
Match plus ExplainMatch
var entity = await db.People.AsNoTracking().FirstAsync(ct);
if (whereClauseService.MatchesWhereClause(entity, where))
{
var explain = whereClauseService.ExplainMatch(entity, where);
// explain.Passed, BlockingPath, FailureSummary, Nodes (per-condition ActualValueSummary)
// In-memory only — not for SQL-only pipelines
IReadOnlyList<Error> errors = explain.ToErrors(); // structured validation errors; empty when Passed
}
Validate paths and collection includes
if (!whereClauseService.TryValidatePropertyPath<Person>("Addresses.City", out var err))
throw new InvalidOperationException(err);
foreach (var path in whereClauseService.GetCollectionIncludePathsForWhereClause<Person>(where))
query = query.Include(path); // e.g. "Addresses" for "Addresses.City"
Fingerprints from WhereClauseHelpers
using Lyo.Query.Services.WhereClause;
var hash = WhereClauseHelpers.GetWhereClauseTreeHash(where); // stable structural fingerprint
var hasSub = WhereClauseHelpers.HasAnySubClause(where);
if (WhereClauseHelpers.TryExtractConditions(where, out var conditions, out var op))
{
// flat And/Or leaves — false when SubClause or unsupported nesting present
}
Diff properties for a patch
var diffs = propertyComparisonService.GetPropertyDifferences(entity, patchDto);
// Dictionary<propertyName, newValue> — only changed writable props
Benchmarks
- Portfolio suite:
query
Package map
| Package | Responsibility |
|---|---|
Lyo.Query (this) |
WhereClause → LINQ / matcher; sort; explain; path metadata; AggregateQuery group-by; ICache for compiled trees |
Lyo.Query.Models |
AST types, QueryConcreteReq / ProjectionQueryReq / QueryReq, builders, enums |
Lyo.Api |
POST …/QueryConcrete, /QueryProject, root /Query; query result caching; projection SQL |
Lyo.Query.Web.Components |
Blazor query workbench |
Builders and endpoint payloads are documented on Models / Api. This doc covers the runtime translator.
QueryProject aggregates
AggregateQuery groups an IQueryable<T> when ProjectionQueryReq.Aggregates is non-empty. Select is the group key. An empty Select is one summary row over the filtered set.
WhereClause filters rows before grouping. SortBy may name a select path or an aggregate output name. Start / Amount page groups, and an exact total is the number of groups.
Count with no field is COUNT(*) (every row in the group). Count of a collection, or of a path ending in .count, totals that collection's elements in the group. Count of a
scalar skips nulls and keeps duplicates. CountDistinct counts distinct values of a scalar field and skips nulls. Sum and Average take a numeric field; Average of an
integral field is double, and decimal stays decimal. Min and Max take a scalar field. An empty ungrouped source returns one row: Count and CountDistinct are 0, a
non-nullable Sum is 0, and a nullable Sum / Average / Min / Max is null. A grouped query over no rows returns no items.
A path segment may end with [n], [start:], [:end], [start:end], or [:]. start is inclusive and end is exclusive. On a string the window is still a string, so it can
be a group key. On a collection, entity elements order by a single [Key] or Id, and scalar elements order by their value. A one-element window is a scalar. A longer collection
window is a select list and is rejected as a group key.
Lyo.Api runs this before the entity projection. If the provider cannot translate the grouping, the call fails instead of grouping a loaded page.
IWhereClauseService
BaseWhereClauseService is the default implementation in DI.
| Member | Behavior |
|---|---|
ApplyWhereClause<T>(source, where, includeSubClauses = true) |
Compiles AST → Expression<Func<T,bool>>, caches under filter_ef_predicate:…, applies .Where. includeSubClauses: false skips SubClause chains (SQL phase). |
SortByProperty<T>(source, propertyName, direction?) |
Single dotted path OrderBy / OrderByDescending (default Desc when null). Collections order by count. |
ApplyOrdering<T>(…, sortByProps, defaultOrder, defaultSortDirection) |
Multi-key sort by SortBy.Priority then list order; always attaches default tie-break for stable paging. |
MatchesWhereClause<T>(entity, where) |
Compiled in-memory matcher (filter_matcher:… cache). |
ExplainMatch<T>(entity, where) |
WhereClauseExplainResult. Per-node pass/fail, BlockingPath, OR-branch outcomes, SubClause chains. In-memory only (default interface throws). |
GetCollectionIncludePathsForWhereClause<T>(where) |
Distinct navigation prefixes that cross collections (for EF Include before sub-clause match). |
TryValidatePropertyPath<T>(name, out error) |
Preflight for sort / filter fields. |
Invalid paths / operators → InvalidQueryException (Lyo.Query.Models.Exceptions).
Comparator and path semantics
Operators come from ComparisonOperatorEnum on each ConditionClause:
| Operator | Notes |
|---|---|
Equals / NotEquals |
Simple comparison; null-safe rules enforced |
GreaterThan / GreaterThanOrEqual / LessThan / LessThanOrEqual |
On scalars: value compare. On collection navigations: compare against collection count |
Contains / NotContains / StartsWith / EndsWith / negations |
String methods when the target type supports them |
In / NotIn |
Value may be a list or CSV string |
Regex / NotRegex |
String regex; trivial patterns like .* short-circuit to true |
Fuzzy / NotFuzzy |
Trigram Jaccard (FuzzyMatch.Similarity); optional Threshold (default 0.3). Postgres emits similarity() via Lyo.Query.Postgres |
Dotted paths
- Scalar:
Status,Address.City - Into collection:
Lines.Quantity→lines.Any(e => e.Quantity …) - Collection count: path metadata
IsCountPathbuildsEnumerable.Countthen compares
Group optimization: within an AND/OR group, multiple Contains leaves on the same string field may be coalesced into a single case-insensitive Regex alternation before
expression build.
Two-phase SubClause
WhereClause.SubClause (on a condition or group) supports split execution. Lyo.Api query pipelines use this often:
- Phase 1 (DB).
ApplyWhereClause(..., includeSubClauses: false)so only the primary predicate becomes SQL. - Load.
GetCollectionIncludePathsForWhereClause→ EFIncludefor collection segments referenced by the sub-tree. - Phase 2 (memory).
MatchesWhereClause/ExplainMatchon materialized entities with the complete tree (includeSubClausesdefault true).
Helpers:
WhereClauseHelpers.HasAnySubClause(node). Detect whether a two-phase path is needed.WhereClauseHelpers.TryExtractConditions. Flatten simple And/Or trees for projection-level filtering (returnsfalseif anySubClause).WhereClauseHelpers.GetWhereClauseTreeHash. Structural fingerprint for cache keys / logging (not cryptographic).
var where = WhereClauseBuilder.And()
.Equals("Age", 10)
.AddSubClause(sub => sub.AddAnd(s => s.Equals("Name", "Alice")))
.Build();
How ICache holds predicates
AddLyoQueryServices requires ICacheService + CacheOptions (e.g. AddLocalCache / AddFusionCache). That is not the same as Lyo.Api query- result caching
(QueryOptions.CacheQueryResultsAsUtf8Payload).
| Cache key prefix / pattern | Stores |
|---|---|
filter_ef_predicate:… |
Compiled Expression / predicate for ApplyWhereClause (keyed by entity + tree + includeSubClauses) |
filter_matcher:… |
Compiled in-memory matcher for MatchesWhereClause |
| Sort-key lambdas | Per (TEntity, propertyName) order key selectors |
| Property-path metadata | Resolved dotted-path segments, collection indexes, final CLR types |
SubQueryIncludePaths_… |
Include-path lists for a where tree |
| Property-comparison strategies | Per-property equality strategy for IPropertyComparisonService |
Tags typically include the entity CLR type so hosts can invalidate by type when schemas change.
Api result cache (optional UTF-8 payload entries for /QueryConcrete + /QueryProject) is documented
under Lyo.Api query result caching.
IPropertyComparisonService
GetPropertyDifferences<TEntity, TOther>(entity, newData) walks public writable properties on TEntity that have a same-named readable property on TOther, compares with
an inferred strategy (direct equality, enum/string coercion, or Convert.ChangeType), and returns Dictionary<string, object?> of changed property → proposed new value.
Patch/update pipelines (e.g. Lyo.Api) use this to build minimal diffs. Strategies are cached via ICacheService.
IValueConversionService
ConvertToTargetType(value, targetType). JSON literals → CLR (primitives, nullables,Guid, enums, date/time, lists)GetUnderlyingType(type). StripNullable<T>IsObjectEnumerable(value). Non-string/ non-byte[]IEnumerable
When hosting Lyo.Api, register Api's ITypeConversionService (extends this interface) and call:
services.AddLyoQueryServices(registerValueConversion: false);
Otherwise two IValueConversionService registrations collide.
Registration
services.AddLocalCache(/* … */); // prerequisite
services.AddLyoQueryServices(registerValueConversion: true);
Registers as singletons:
IValueConversionService→ValueConversionService(unlessregisterValueConversion: false)IPropertyComparisonService→PropertyComparisonServiceIWhereClauseService→BaseWhereClauseService
Optional: IMetrics is taken from DI when present and forwarded only if CacheOptions.EnableMetrics is set.
Metrics
Recording lives on WhereClauseEvaluator in Lyo.Query.Evaluation, so these fire whichever implementation you use. BaseWhereClauseService
forwards the container's IMetrics only when CacheOptions.EnableMetrics is true; otherwise nothing is recorded. Names come from Lyo.Query.Constants.Metrics:
| Name | Kind |
|---|---|
query.filter.apply_query_node.duration |
histogram/timer |
query.filter.apply_query_node.success |
counter |
query.filter.sort_by_property.duration |
histogram/timer |
query.filter.sort_by_property.success |
counter |
query.filter.apply_ordering.duration |
histogram/timer |
query.filter.apply_ordering.success |
counter |
query.filter.matches_query_node.duration |
histogram/timer |
query.filter.matches_query_node.success |
counter |
query.filter.sort_by_count |
gauge |
Each is tagged entity_type (Constants.Metrics.Tags.EntityType).
Related docs
- DTOs / builders / request shapes →
Lyo.Query.Models - HTTP endpoints, projection SQL, result caching →
Lyo.ApiQuery & Request Builders - Blazor workbench →
Lyo.Query.Web.Components - Portfolio benchmarks suite →
query
Links
Dependencies
Generated from ProjectReference / PackageReference (same model as docs/Lyo.ProjectGraph.html).
Lyo.Cache(direct, lyo)Lyo.Common.Core(direct, lyo)Lyo.Exceptions(direct, lyo)Lyo.Metrics(direct, lyo)Lyo.Query.Evaluation(direct, lyo)Lyo.Query.Models(direct, lyo)Microsoft.Extensions.DependencyInjection.Abstractions10.0.5(direct, microsoft)Lyo.Common.Metadata(transitive, lyo)Lyo.Compression(transitive, lyo)Lyo.Encryption(transitive, lyo)Lyo.Hashing(transitive, lyo)Lyo.Health(transitive, lyo)Lyo.Json(transitive, lyo)Lyo.KeyStore(transitive, lyo)Lyo.Parameters(transitive, lyo)Lyo.Result(transitive, lyo)Lyo.Streams(transitive, lyo)BouncyCastle.Cryptography2.6.2(transitive, third-party, netstandard2.0)EasyCompressor2.1.0(transitive, third-party)Konscious.Security.Cryptography.Argon21.3.1(transitive, third-party)Microsoft.Bcl.AsyncInterfaces10.0.5(transitive, microsoft, netstandard2.0)Microsoft.Extensions.Caching.Memory10.0.5(transitive, microsoft)Microsoft.Extensions.Configuration.Binder10.0.5(transitive, microsoft)Microsoft.Extensions.DependencyInjection10.0.5(transitive, microsoft)Microsoft.Extensions.Logging.Abstractions10.0.5(transitive, microsoft)Microsoft.Extensions.Options.ConfigurationExtensions10.0.5(transitive, microsoft)System.Buffers4.6.1(transitive, microsoft, netstandard2.0)System.IO.Hashing10.0.5(transitive, microsoft, net10.0)System.Memory4.6.3(transitive, microsoft, netstandard2.0)System.Text.Encoding.CodePages10.0.5(transitive, microsoft)System.Text.Json10.0.5(transitive, microsoft, netstandard2.0)System.Threading.Tasks.Extensions4.6.3(transitive, microsoft, netstandard2.0)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Lyo.Cache (>= 3.0.0)
- Lyo.Common.Core (>= 3.0.0)
- Lyo.Exceptions (>= 3.0.0)
- Lyo.Metrics (>= 3.0.0)
- Lyo.Query.Evaluation (>= 3.0.0)
- Lyo.Query.Models (>= 3.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Lyo.Query:
| Package | Downloads |
|---|---|
|
Lyo.Api
Core API library for building RESTful APIs with Entity Framework Core, caching, and mapping support. |
|
|
Lyo.Leaderboard.Api
HTTP API for Lyo.Leaderboard. Embed via AddLeaderboardApi / MapLeaderboardApiEndpoints. |
|
|
Lyo.Query.Postgres
PostgreSQL translation for Lyo.Query Fuzzy operators via pg_trgm similarity(). |
GitHub repositories
This package is not used by any popular GitHub repositories.