DynamicWhere.ex.Policies.EntityFrameworkCore 3.0.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DynamicWhere.ex.Policies.EntityFrameworkCore" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DynamicWhere.ex.Policies.EntityFrameworkCore" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DynamicWhere.ex.Policies.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DynamicWhere.ex.Policies.EntityFrameworkCore --version 3.0.0
                    
#r "nuget: DynamicWhere.ex.Policies.EntityFrameworkCore, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DynamicWhere.ex.Policies.EntityFrameworkCore@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DynamicWhere.ex.Policies.EntityFrameworkCore&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DynamicWhere.ex.Policies.EntityFrameworkCore&version=3.0.0
                    
Install as a Cake Tool

DynamicWhere.ex

JSON-driven queries for Entity Framework Core.

NuGet Version NuGet Downloads License .NET Docs

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 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.
  • 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* 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 ✓).


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.Redis and Policies.EntityFrameworkCore hold rules; Policies.AspNetCore mounts 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> and SummaryResult each gain one nullable Policy property, 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. MinGroupSize ships 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, 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.


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.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.2.0 37 9/20/2026
3.1.0 42 9/18/2026
3.0.0 77 9/14/2026

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.