Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet VersionNuGet DownloadsLicense.NETDocs

A powerful, versatile library for dynamically composing complex filter, sort, paginate, group, aggregate, and set-operation (Union / Intersect / Except) expressions in Entity Framework Core applications — all driven by simple JSON objects from any front-end or API consumer.

Full reference, JSON cookbook, and tuning guide → doc.dynamicwhere.com


Why DynamicWhere.ex?

Stop concatenating LINQ predicates by hand. Your front-end sends one JSON shape; the back-end calls a single extension method. You get back a strongly-typed, paginated result.

  • JSON in → IQueryable<T> out. No string LINQ. No manual expression trees.
  • Three composable shapesFilter, Segment, Summary — cover where, set operations, and group-by reporting.
  • Seventeen extension methods on IQueryable<T> and IEnumerable<T>.
  • Nested navigation through references and collections, with auto-wrapped .Any() lambdas where needed.
  • Heterogeneous Condition.Values — pass raw numbers, booleans, strings; normalized per DataType.
  • Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
  • Free Forever. Targets .NET 6, 7, 8, 9.

Install

dotnet add package DynamicWhere.ex --version 2.1.5

Or via Package Manager:

Install-Package DynamicWhere.ex -Version 2.1.5

Dependencies (restored automatically):

PackageVersion
Microsoft.EntityFrameworkCore6.0.22
System.Linq.Dynamic.Core1.6.7

Quick Start

Front-end / API body — pure JSON:

{
"conditionGroup": {
"connector": "And",
"conditions": [
{ "sort": 1, "field": "Price", "dataType": "Number", "operator": "GreaterThan", "values": [50] },
{ "sort": 2, "field": "Category.Name", "dataType": "Text", "operator": "IEqual", "values": ["electronics"] }
],
"subConditionGroups": []
},
"selects": ["Id", "Name", "Price", "Category.Name"],
"orders": [{ "sort": 1, "field": "Price", "direction": "Descending" }],
"page": { "pageNumber": 1, "pageSize": 10 }
}

Back-end — one method call:

usingDynamicWhere.ex.Source;usingDynamicWhere.ex.Classes.Complex;app.MapPost("/products/search",async(Filterfilter,AppDbContextdb)=>{FilterResult<Product>result=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);});

Response shape (FilterResult<Product>):

{
"pageNumber": 1,
"pageSize": 10,
"pageCount": 5,
"totalCount": 42,
"data": [
{ "id": 7, "name": "Laptop Pro", "price": 1299.99, "category": { "name": "Electronics" } }
],
"queryString": null
}

That's the whole loop. Full walk-through in Quick Start.


What's inside

Three composable shapes

ShapePipelineUse when
Filterwhere → order → page → selectStandard list / search / detail endpoints
Segmentset1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → pageUNION / INTERSECT / EXCEPT across multiple condition sets
Summarywhere → group → having → order → pageAggregate reporting (GROUP BY + SUM / AVG / COUNT …)

Seventeen extension methods

Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:

GroupMethods
Projection.Select<T>(fields) · .SelectDynamic<T>(fields)
Filtering.Where<T>(Condition) · .Where<T>(ConditionGroup)
Composition.Order<T> · .Page<T> · .Group<T> · .Filter<T> · .FilterDynamic<T> · .Summary<T>
Materialization.ToList<T>(Filter) · .ToListAsync<T>(Filter) · .ToListDynamic<T>(Filter) · .ToListAsyncDynamic<T>(Filter) · .ToList<T>(Summary) · .ToListAsync<T>(Summary) · .ToListAsync<T>(Segment)

Full signatures, validations, and return types → Extension Methods Reference.

Operators & data types

Twenty-eight comparison operators across seven data types — case-sensitive and case-insensitive variants of every text operation:

  • Equality:Equal · IEqual · NotEqual · INotEqual
  • Substring:Contains · IContains · NotContains · INotContains · StartsWith · IStartsWith · EndsWith · IEndsWith (+ Not* and I* of each)
  • Set:In · IIn · NotIn · INotIn
  • Range:GreaterThan · GreaterThanOrEqual · LessThan · LessThanOrEqual · Between · NotBetween
  • Null:IsNull · IsNotNull

Data types: Text · Guid · Number · Boolean · DateTime · Date · Enum. Full matrix → DataType reference.

Aggregations

Count · CountDistinct · Sumation · Average · Minimum · Maximum · FirstOrDefault · LastOrDefault. With optional Having post-filter referencing aggregate aliases.

Nested navigation

Dotted paths through reference and collection properties, with .Any() lambdas inserted automatically where the path crosses a collection.

{ "field": "Orders.OrderItems.ProductName", "dataType": "Text",
"operator": "IContains", "values": ["laptop"] }

Becomes:

Orders.Any(i1 => i1.OrderItems.Any(i2 =>
i2.ProductName != null && i2.ProductName.ToLower().Contains("laptop")))

Sorting takes the same paths. .Any() yields a boolean, so ordering reduces each collection segment to one comparable value instead — the smallest element ascending, the largest descending:

{ "sort": 1, "field": "OrderItems.Product.Name", "direction": "Ascending" }

Becomes:

OrderItems.Min(Product.Name) asc

Rows with an empty collection sort as null (or the type default for non-nullable value types). A path may not end on a collection of entities — sort by a scalar inside it (Tags ✗ → Tags.Value ✓).


Reflection cache

A thread-safe ConcurrentDictionary-backed cache across three stores (TypeProperties · PropertyPath · CollectionElementType) eliminates reflection overhead on repeated queries. Three eviction strategies and six tuned presets:

PresetMaxSizeEvictionUse case
Default1000LRUGeneral purpose
ForHighMemoryEnvironment()5000LRUServers with ample RAM
ForLowMemoryEnvironment()250LFUConstrained environments
ForDevelopment()100FIFOTesting & debugging
ForHighFrequencyAccess()2000LFURepeated queries on same types
ForTemporalAccess()1500LRURecent-access-heavy workloads
usingDynamicWhere.ex.Optimization.Cache.Source;CacheExpose.Configure(CacheOptions.ForHighMemoryEnvironment());CacheExpose.WarmupCache<Product>("Name","Category.Name","Price");

Full tuning guide → Cache & Optimization.


Error handling

Every validation failure throws LogicException with a structured error code. Catch at your API boundary and surface as a 400:

try{varresult=awaitdb.Products.ToListAsync(filter);returnResults.Ok(result);}catch(LogicExceptionex){returnResults.BadRequest(new{code=ex.Message});}

Full code reference → Error Codes.


Documentation

The complete reference — every enum, class, extension method, validation rule, JSON example, and cache option — lives on the official site:

SectionWhat's there
Getting StartedIntroduction, installation, quick start
EnumsEvery DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum
ClassesCondition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types
Extension MethodsAll 17 methods with signatures, validations, examples
Validation RulesWhat's checked and what throws
JSON Cookbook13 copy-pasteable end-to-end examples
Cache & OptimizationArchitecture, stores, options, presets, monitoring
Error CodesEvery LogicException message
Breaking ChangesKnown limits and migration notes

Version 2.1.5 highlights

  • Fixed: the XML documentation shipped with the package. It drives IntelliSense in your IDE, and three defects degraded it — an unescaped generic argument in the Select<T> comment truncated its remarks and returns text, three ToList / ToListAsync overloads were missing the getQueryString description, and CacheReporting.GetQuickHealthSummary documented a parameter it does not take. The library now builds with zero warnings. No API or behaviour changes.

Version 2.1.4 highlights

Security and correctness fix — upgrade recommended for everyone.

  • Fixed: values carrying a backslash or a double quote broke the query. Condition values are embedded in the generated dynamic LINQ expression as string literals, and were not escaped. A search term ending in \ — the reported case was an Arabic term typed into a search box — escaped its own closing quote, so the parser ran on into the rest of the expression and threw System.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally, \ and " included, across every Text and Enum operator.
  • Fixed: a crafted value could rewrite the predicate. The same missing escape let a value close its literal and append clauses of its own — x") || (1==1) || Name.Contains("y turned a Contains filter into an always-true predicate and returned every row. Values can no longer break out of their literal.
  • Fixed: AggregateBy.Alias could inject extra projection columns. The alias was only checked for dots, so "Total, 1 as Leaked" appended a term to the generated Select. Aliases must now be plain identifiers — a leading letter or underscore, then letters, digits, or underscores, with non-Latin letters allowed. Anything else already failed to parse, so nothing that worked is rejected; malformed aliases now throw AggregationMustHasValidAlias at validation time.

Version 2.1.3 highlights

  • MIT licensed. Free forever for commercial and personal use, no license acceptance required.

Version 2.1.2 highlights

  • Fixed: ordering across collection navigations.OrderBy.Field = "Tags.Value" on a List<Tag> threw No property or field 'Value' exists in type 'List\1'. Collection segments are now reduced to a single comparable value — Minascending,Max` descending — at any nesting depth. See Nested navigation.

Version 2.1.0 highlights

  • Heterogeneous Condition.ValuesList<object> with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible with List<string> callers.
  • Six tuned cache presets — pick ForHighMemory, ForLowMemory, ForDevelopment, ForHighFrequencyAccess, ForTemporalAccess, or the default.
  • Official documentation site launched at doc.dynamicwhere.com.

See Breaking Changes & Known Limitations for the complete migration / caveat list.


Compatibility

  • .NET: 6, 7, 8, 9
  • EF Core providers: SQL Server, PostgreSQL (Npgsql), MySQL (Pomelo), SQLite — anything that supports ToQueryString() for the optional getQueryString: true flag.
  • Enum storage: assumed stored as strings. Use DataType.Number if your column stores integers.
  • Case-insensitive operators: emit .ToLower() on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQL C locale.

Links


License

MITFree Forever. Copyright © 2023-2026 Sajjad H. Al-Khafaji.

Free for commercial and personal use, forever. No license acceptance required, no attribution beyond keeping the copyright notice, no restrictions on redistribution.

About

JSON-driven queries for Entity Framework Core — dynamic filter, sort, paginate, project, group, aggregate, and UNION / INTERSECT / EXCEPT set operations, composed from simple JSON sent by any front-end or API consumer. Targets .NET 6+.

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Used by

Contributors

Languages