DynamicWhere.ex.Policies.EntityFrameworkCore
3.0.0
See the version list below for details.
dotnet add package DynamicWhere.ex.Policies.EntityFrameworkCore --version 3.0.0
NuGet\Install-Package DynamicWhere.ex.Policies.EntityFrameworkCore -Version 3.0.0
<PackageReference Include="DynamicWhere.ex.Policies.EntityFrameworkCore" Version="3.0.0" />
<PackageVersion Include="DynamicWhere.ex.Policies.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="DynamicWhere.ex.Policies.EntityFrameworkCore" />
paket add DynamicWhere.ex.Policies.EntityFrameworkCore --version 3.0.0
#r "nuget: DynamicWhere.ex.Policies.EntityFrameworkCore, 3.0.0"
#:package DynamicWhere.ex.Policies.EntityFrameworkCore@3.0.0
#addin nuget:?package=DynamicWhere.ex.Policies.EntityFrameworkCore&version=3.0.0
#tool nuget:?package=DynamicWhere.ex.Policies.EntityFrameworkCore&version=3.0.0
DynamicWhere.ex
JSON-driven queries for Entity Framework Core.
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
Using an AI coding agent?
Point it at doc.dynamicwhere.com/llms.txt — the entire API surface in one plain-text file: every shape and field name, every enum member verbatim, all seventeen methods, the whole policy layer, and the traps that produce code which compiles and is quietly wrong.
Read https://doc.dynamicwhere.com/llms.txt before writing any
DynamicWhere.ex code. It is the complete API surface.
Works with Claude, Copilot, Cursor, Codex or anything else that can read a URL. If yours cannot, copy it from the reference page.
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 shapes —
Filter,Segment,Summary— cover where, set operations, and group-by reporting. - Seventeen extension methods on
IQueryable<T>andIEnumerable<T>. - Nested navigation through references and collections, with auto-wrapped
.Any()lambdas where needed. - Heterogeneous
Condition.Values— pass raw numbers, booleans, strings; normalized perDataType. - Thread-safe reflection cache with FIFO / LRU / LFU eviction and six tuned presets.
- Field-level policies (new in 3.0) — decide per caller what may be filtered, sorted, selected, grouped, aggregated and seen. Opt-in: nothing enforces until you ask.
- Free Forever. Targets .NET 6, 7, 8, 9, 10.
Install
dotnet add package DynamicWhere.ex --version 3.0.0
Or via Package Manager:
Install-Package DynamicWhere.ex -Version 3.0.0
Dependencies (restored automatically):
| Package | Version |
|---|---|
Microsoft.EntityFrameworkCore |
6.0.22 |
System.Linq.Dynamic.Core |
1.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:
using DynamicWhere.ex.Source;
using DynamicWhere.ex.Classes.Complex;
app.MapPost("/products/search", async (Filter filter, AppDbContext db) =>
{
FilterResult<Product> result = await db.Products.ToListAsync(filter);
return Results.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
| Shape | Pipeline | Use when |
|---|---|---|
Filter |
where → order → page → select | Standard list / search / detail endpoints |
Segment |
set1 ∪/∩/∖ set2 ∪/∩/∖ set3 → order → page | UNION / INTERSECT / EXCEPT across multiple condition sets |
Summary |
where → group → having → order → page | Aggregate reporting (GROUP BY + SUM / AVG / COUNT …) |
Seventeen extension methods
Projection, filtering, composition, and materialization on IQueryable<T> and IEnumerable<T>:
| Group | Methods |
|---|---|
| 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*andI*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 ✓).
Field-level policies
New in 3.0. Entirely opt-in — a project with no policy attributes and no DwPolicy.Configure call behaves exactly as 2.1.5.
The library accepts a JSON Filter from any caller and turns it into a query. Policies add the missing question: who is asking, and what are they allowed to see?
// Once, at startup.
DwPolicy.Configure(new DwPolicyOptions { Tier = DwTier.Convenience, HashSalt = secret });
// Once per request.
var caller = await DwPolicy.PrepareAsync(
new DwPolicyContext()
.WithSubject(DwSubjectKind.User, userId)
.WithSubject(DwSubjectKind.Role, "Support"));
// Then query through the guarded handle instead of the raw IQueryable.
var result = await db.Employees.ApplyPolicy(caller).ToListAsync(filter);
Requests are sanitized before the query is built; results are transformed after they materialize. The query engine itself is unchanged.
[DwEntity(RequirePolicy = true)] // an unguarded read throws instead of returning rows
public class Employee
{
[DwMask(MaskStrategy.Email), DwNoOrder]
public string Email { get; set; } // s*************@c******.com on the way out
[DwForceWhere(Operator.Equal, Value = "true")]
public bool IsActive { get; set; } // ANDed into every guarded query, asked for or not
[DwGeneralize(GeneralizeMode.Round, Step = 5000, AllowAggregate = true, MinGroupSize = 5)]
[DwNoOrder, DwAudit, DwCost(10)]
public decimal Salary { get; set; } // rounded, aggregatable only over groups of 5+
[DwDenied]
public JsonDocument? WorkSchedule { get; set; } // absent from /schema, rejected by POST /rules
}
Six features, per field: Where · Select · Order · Group · Aggregate · Segment.
| Attribute | What it does |
|---|---|
[DwDeny], [DwDenied], [DwNoWhere], [DwNoSelect], [DwNoOrder], [DwNoGroup], [DwNoAggregate] |
Refuse features for a field |
[DwOperators] |
Restrict which operators may target it |
[DwAlias] |
Give it a public name, renamed back on the way out |
[DwForceWhere] |
Add a predicate to every guarded query — tenant scope, soft delete, ownership |
[DwRequireWhere] |
Make a filter on it mandatory |
[DwMask] |
Obscure the value — 9 strategies: Full Partial Email Phone Regex Fixed Hash Null Tokenize |
[DwMutate], [DwDefault], [DwGeneralize], [DwTruncate], [DwFormat] |
The other five transforms |
[DwDescribe], [DwAllowedValues], [DwCost], [DwAudit] |
Schema discovery, query budget, audit trail |
Sealed by default
Attributes cannot be lifted by a runtime rule unless you mark them Overridable = true. Six precedence levels decide every field, sealed attributes first and overridable attributes last, with dynamic user, role, tenant and global rules in between.
Configuration, and a field picker that fits on a screen
The whole posture binds from appsettings.json, environment variables or a vault. A key nothing answers to refuses to start, because a misspelt MinGropSize sitting in a file doing nothing is exactly the failure the rest of this layer exists to prevent.
builder.Services.AddDwPolicies(
builder.Configuration.GetSection("DynamicWhere:Policies"),
options => options.Entities.Expose<Employee>("Employee"));
POST /dw-policies/schema describes an entity for a filter UI, two levels deep by default and drillable a subtree at a time. The response is flat with a parent on every field and node, so a tree is one grouping pass on the client. → Admin API
Rules without a redeploy
An optional store supplies rules at runtime, split into a cached broad zone and a per-request narrow zone. In-memory ships in the core package; Redis and Entity Framework Core are separate packages. A store can never grant a field the source code seals.
The control you would not guess: MinGroupSize
SUM, MAX and MIN run in SQL, against the stored value, before any mask can apply — so MAX(Salary) over a department of one returns that person's exact pay. Aggregating a transformed field is therefore denied by default, opted into with AllowAggregate = true, and bounded by MinGroupSize, which suppresses any group smaller than k.
It defaults to 5. Write MinGroupSize = 1 to switch it off and it is off, in production, with nothing refused and nothing warned about — the setting starts unset rather than at one precisely so that "off" and "never configured" stay different sentences. → Security & k-anonymity
Hiding a value you still want to group by
Hash and Tokenize both keep a column groupable and joinable while hiding what is in it. The difference is where the secret lives.
A hash is computed from the value, with HMAC-SHA256 keyed by HashSalt — at least 16 characters, or it is refused where it is written. Whoever holds that salt can recompute every digest the deployment ever emitted.
A token is drawn at random and written into TokenVault, so the only way back is to read the vault: a store you can lock, move and revoke separately from the data. Three ship — in-memory in the core package, Redis and Entity Framework Core in the providers — all held to one conformance suite.
new DwPolicyOptions { HashSalt = secret, TokenVault = new RedisTokenVault(redis) }
Neither closes equality, and that is the point of both: the same value maps to the same output so the column stays usable, which also means anyone who can write a chosen value and read it back learns that one value's stand-in. → Transforms
The four packages
| Package | What it adds |
|---|---|
DynamicWhere.ex |
Everything above |
DynamicWhere.ex.Policies.Redis |
Rules in Redis, pub/sub invalidation with a poll behind it |
DynamicWhere.ex.Policies.EntityFrameworkCore |
Rules in any EF Core provider |
DynamicWhere.ex.Policies.AspNetCore |
Admin API — schema, rules, explain, simulate, health. Refuses to mount without a named authorization policy |
Full guide → doc.dynamicwhere.com/docs/policies
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:
| Preset | MaxSize | Eviction | Use case |
|---|---|---|---|
Default |
1000 | LRU | General purpose |
ForHighMemoryEnvironment() |
5000 | LRU | Servers with ample RAM |
ForLowMemoryEnvironment() |
250 | LFU | Constrained environments |
ForDevelopment() |
100 | FIFO | Testing & debugging |
ForHighFrequencyAccess() |
2000 | LFU | Repeated queries on same types |
ForTemporalAccess() |
1500 | LRU | Recent-access-heavy workloads |
using DynamicWhere.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
{
var result = await db.Products.ToListAsync(filter);
return Results.Ok(result);
}
catch (LogicException ex)
{
return Results.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:
→ doc.dynamicwhere.com
| Section | What's there |
|---|---|
| Getting Started | Introduction, installation, quick start |
| Enums | Every DataType, Operator, Connector, Direction, Intersection, Aggregator, Cache enum |
| Classes | Condition, ConditionGroup, ConditionSet, OrderBy, GroupBy, AggregateBy, PageBy, Filter, Segment, Summary, Result types |
| Extension Methods | All 17 methods with signatures, validations, examples |
| Validation Rules | What's checked and what throws |
| JSON Cookbook | 13 copy-pasteable end-to-end examples |
| Field-Level Policies | Attributes, precedence, masking, dynamic rules, admin API, k-anonymity |
| Cache & Optimization | Architecture, stores, options, presets, monitoring |
| Error Codes | Every LogicException message |
| Breaking Changes | Known limits and migration notes |
Version 3.0.0 highlights
- New: field-level policies. A layer that decides what each caller may filter, sort, select, group, aggregate and see — attributes for the compile-time half, an optional store for the runtime half. See above.
- New: three companion packages.
Policies.RedisandPolicies.EntityFrameworkCorehold rules;Policies.AspNetCoremounts the admin API, explain, simulate and health, and refuses to map without a named authorization policy. - No breaking changes. The 2.x API is untouched.
FilterResult<T>andSummaryResulteach gain one nullablePolicyproperty, null when the query was not guarded. Nothing enforces until you opt in. - Worth knowing before you turn it on: gating costs nothing measurable, but transforming every row of a large result costs about 1.6x in time and 7x in allocations, because each value is rebuilt after materialization rather than in SQL.
MinGroupSizeships on at 5, so a guarded summary suppresses groups under five until you say otherwise — see Security and Configuration.
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, threeToList/ToListAsyncoverloads were missing thegetQueryStringdescription, andCacheReporting.GetQuickHealthSummarydocumented 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 threwSystem.Linq.Dynamic.Core.Exceptions.ParseException: ')' or ',' expected. Values are now escaped and matched literally,\and"included, across everyTextandEnumoperator. - 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("yturned aContainsfilter into an always-true predicate and returned every row. Values can no longer break out of their literal. - Fixed:
AggregateBy.Aliascould inject extra projection columns. The alias was only checked for dots, so"Total, 1 as Leaked"appended a term to the generatedSelect. 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 throwAggregationMustHasValidAliasat 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 aList<Tag>threwNo 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.Values—List<object>with type-safe coercion. Send raw numbers and booleans without quoting. Backward-compatible withList<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 optionalgetQueryString: trueflag. - Enum storage: assumed stored as strings. Use
DataType.Numberif your column stores integers. - Case-insensitive operators: emit
.ToLower()on both sides. Works well on SQL Server's default collation; watch for case-sensitive PostgreSQLClocale.
Links
- Documentation: doc.dynamicwhere.com
- NuGet: nuget.org/packages/DynamicWhere.ex
- Source: github.com/Sajadh92/DynamicWhere.ex
- Issues: github.com/Sajadh92/DynamicWhere.ex/issues
License
MIT — Free 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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- DynamicWhere.ex (>= 3.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 6.0.22)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v3.0.0 — First release. A database-backed policy store for the DynamicWhere.ex field-level policy layer, introduced in DynamicWhere.ex 3.0.0. Works with any EF Core provider; ships DwPolicyDbContext, the rule and version records, and their EF configurations. Passes the same store conformance suite as the in-memory and Redis stores, verified against PostgreSQL.
THREE TABLES, NOT TWO. Alongside DwPolicyRules and DwPolicyVersion, this release adds DwPolicyTokens and DwPolicyTokenConfiguration — the vault behind MaskStrategy.Tokenize, read by EfTokenVault. A consumer applying the configurations to their own DbContext must apply all three and generate a migration for the new table; a deployment that never tokenizes can leave it out rather than migrating in a table it will never write to. The table holds the scope in the clear and a digest of the value, never the value itself, so it is an index into a secret rather than a searchable copy of the column it protects. Guard it as you would guard that column: reading it turns every token in every result back into what it stands for.
EfTokenVault caches every mapping it resolves, which is safe because a token is written once and never rewritten, and settles a concurrent mint through the primary key rather than by retrying. Requires DynamicWhere.ex 3.0.0.