Zift.EntityFrameworkCore
0.9.0
See the version list below for details.
dotnet add package Zift.EntityFrameworkCore --version 0.9.0
NuGet\Install-Package Zift.EntityFrameworkCore -Version 0.9.0
<PackageReference Include="Zift.EntityFrameworkCore" Version="0.9.0" />
<PackageVersion Include="Zift.EntityFrameworkCore" Version="0.9.0" />
<PackageReference Include="Zift.EntityFrameworkCore" />
paket add Zift.EntityFrameworkCore --version 0.9.0
#r "nuget: Zift.EntityFrameworkCore, 0.9.0"
#:package Zift.EntityFrameworkCore@0.9.0
#addin nuget:?package=Zift.EntityFrameworkCore&version=0.9.0
#tool nuget:?package=Zift.EntityFrameworkCore&version=0.9.0

Zift adds dynamic filtering and pagination to IQueryable<T> using familiar LINQ-style methods.
- Dynamic filtering: string expressions with
Where(...) - Pagination: offset-based and cursor-based pagination
Installation
dotnet add package Zift
If you use Entity Framework Core, install the EF Core package instead. It includes Zift and adds the async methods (ToPageAsync and ToCursorPageAsync):
dotnet add package Zift.EntityFrameworkCore
Zift targets .NET 8, 9, and 10. The EF Core package uses the EF Core major version that matches your target framework.
Dynamic Filtering
Filter queries using string expressions. Zift translates them into LINQ expression trees.
var categories = context.Categories
.Where("Products:any(Reviews:any(Rating >= 4))")
.ToList();
Supported Syntax
Property navigation
Name,Author.EmailLogical operators
&&,||,!Example:
Price > 100 && Price < 500Grouping
(...)for evaluation orderExample:
(Price > 100 && Price < 500) || IsFeatured == trueComparison operators
==,!=,<,<=,>,>=Example:
Rating >= 4String operators
%=(contains),^=(starts with),$=(ends with)Example:
Title %= "C#"inoperator with list literalsExample:
Status in ["Active", "Pending"]Quantifiers on collections
:any(predicate):all(predicate):any()(existence check)
Example:
Reviews:any(Author.Email $= "@example.com")Collection projections
:count
Example:
Products:count >= 2
Literals
- Strings use single or double quotes:
"Active"or'Active'. Supported escapes are\\,\", and\'. - Numbers include integers, decimals, and exponents:
42,-1.5,1e3. A leading+or-is optional. - Booleans and null use
true,false, andnull. - Dates, times, and GUIDs use quoted strings:
"2024-03-05T10:20:30Z","2024-03-05", or"10:20:30", depending on the property type. All standard GUID formats are supported. - Enums use quoted, case-sensitive member names:
Status == "Active". Numeric strings aren't supported. For[Flags]enums, separate names with commas.
Numbers must fit the property's type. For an int property, 1.0 and 1e3 are valid; 1.5 and 3000000000 (exceeds int.MaxValue) aren't. Fractional values aren't rounded to integers.
Null and Empty Lists
Use == null or != null to check for null. Other direct comparisons with null aren't supported.
Lists can include null when the property is nullable:
items.Where("Rating in [4, 5, null]");
An empty list matches nothing; negating it matches everything. A list containing only null matches null values.
With EnableNullGuards, a missing navigation counts as null instead of throwing, and comparisons follow normal C# rules. Author.Name == null matches rows without an author, Author.Name != "Ann" includes them, and Author.Rating > 3 doesn't.
A null collection counts as empty: Reviews:count == 0 matches it, Reviews:any() doesn't, and Reviews:all(...) does. A value type behind an optional navigation (Category.Rank == null) can only be compared with null when guards are on.
Options and Limits
Pass ExpressionBuilderOptions to Where(...) to change the defaults:
var options = new ExpressionBuilderOptions
{
EnableNullGuards = true,
MaxExpressionLength = 8192
};
var products = items.Where("Category.Name == \"Books\"", options);
| Option | Default | Description |
|---|---|---|
EnableNullGuards |
false |
Treats a null navigation as null instead of throwing. Useful for in-memory queries. |
ParameterizeValues |
true |
Captures values so providers such as EF Core can parameterize queries and reuse query plans. |
MaxExpressionLength |
4096 |
Filter length limit, in characters. |
MaxNestingDepth |
64 |
Nesting limit for groups, !, and quantifier predicates. |
MaxListItems |
1000 |
List size limit. |
Limits must be at least 1. Keep them bounded when accepting filters from users.
Security
Filters can access any public property, including nested ones. Zift doesn't enforce an allowlist.
When filters come from users:
- Filter over a projection that only has the properties users are allowed to filter on, not the full entity.
- Restrict the query to records the user is allowed to access before applying their filter.
- Remember that filtering reveals values. A user who can't read a property can still probe it with
Email ^= "a". - Keep the limits above bounded.
Errors
Invalid filters throw InvalidFilterException, which derives from FormatException. This includes syntax errors, unknown properties, invalid values, unsupported operators, and exceeded limits.
try
{
var products = items.Where(filter);
}
catch (InvalidFilterException ex)
{
// Tell the caller what's wrong: ex.Message, and ex.Position when it has one.
}
SyntaxErrorException and LexicalErrorException derive from InvalidFilterException. Position is set for those two and is null for the rest. When a .NET parse, conversion, or lookup was behind the error, the original exception is in InnerException.
A null filter throws ArgumentNullException. An empty or whitespace-only one throws ArgumentException.
Note: Errors the database provider raises when the query runs, such as EF Core not being able to translate a property, aren't wrapped.
Pagination
Zift supports two ways to paginate IQueryable<T> queries:
- Offset-based pagination
- Cursor-based pagination
Offset Pagination
Offset pagination uses 1-based page numbers and LINQ's Skip / Take. It works best for smaller datasets where you need a total count.
var page = await context.Categories
.OrderBy(c => c.Name)
.ToPageAsync(pageNumber: 1, pageSize: 25);
Use ToPageAsync with Entity Framework Core. For other sources, such as in-memory collections, use ToPage:
var page = categories
.AsQueryable()
.OrderBy(c => c.Name)
.ToPage(pageNumber: 1, pageSize: 25);
Use a consistent ordering to avoid skipped or repeated rows between pages. pageNumber and pageSize must be at least 1. An offset greater than int.MaxValue throws ArgumentOutOfRangeException.
The count and items come from separate queries, so changes between them can leave the count out of sync with the items.
Result Shape
page.Items
page.PageNumber
page.PageSize
page.PageCount
page.HasNextPage
page.HasPreviousPage
page.TotalItemCount
Use page.Map(...) to convert the items while keeping the paging information, and Page<T>.Empty(pageSize) for an empty first page:
var dtoPage = page.Map(category => new CategoryDto(category));
Cursor Pagination
Cursor pagination (keyset pagination) uses ordering keys instead of offsets to navigate through results. It performs well even on large datasets.
Start by calling AsCursorQuery() on the IQueryable<T>.
var query = context.Products
.AsCursorQuery()
.OrderBy("Name ASC, Price DESC, Id ASC");
var page = query.ToCursorPage(pageSize: 25);
With Entity Framework Core, use ToCursorPageAsync instead:
var page = await query.ToCursorPageAsync(pageSize: 25);
Note: Ordering for cursor pagination must be applied after calling
AsCursorQuery(). Any ordering applied beforeAsCursorQuery()is ignored.
Cursor pagination needs at least one ordering clause.
Ordering
You can also use key selectors for ordering. Add more keys with ThenBy, ThenByDescending, or ThenBy(string):
var query = context.Products
.AsCursorQuery()
.OrderBy(p => p.Name)
.ThenByDescending(p => p.Price)
.ThenBy(p => p.Id);
In the string syntax, use dots for nested paths (Category.Title). Names aren't case-sensitive. Each entry is Property [ASC|DESC]. A bad entry or unknown property throws InvalidOrderingClauseException. For in-memory sources, pass new OrderingOptions { EnableNullGuards = true } to guard nested paths against null.
Ordering keys can be strings, numbers, booleans, enums, or any type with comparison operators or IComparable<T> (Guid, DateTime, DateTimeOffset). Unsupported types throw NotSupportedException when navigating beyond the first page.
The ordering must uniquely identify each row, so end it with a unique key such as Id. Rows that tie on every key can be skipped between pages.
By default, Zift assumes nulls sort first in ascending order (SQL Server, SQLite, MySQL). For databases that sort them last (PostgreSQL, Oracle), pass new CursorQueryOptions { NullOrdering = NullOrdering.NullsCompareHigher } to AsCursorQuery().
Navigating
Cursors are URL-safe tokens tied to the entity type and ordering that produced them. Using one with a different ordering or entity type throws InvalidCursorException. They aren't encrypted or signed.
Reuse the ordered query and pass the current page's cursors to navigate. A null cursor selects the first page.
// Forward traversal
if (page.HasNextPage)
{
var nextPage = query
.After(page.EndCursor)
.ToCursorPage(pageSize: 25);
}
// Backward traversal
if (page.HasPreviousPage)
{
var previousPage = query
.Before(page.StartCursor)
.ToCursorPage(pageSize: 25);
}
HasPreviousPage is assumed true after After(...), and HasNextPage after Before(...). No extra query is made to verify this, so changes between requests can make these values inaccurate.
Page Size
pageSize must be between 1 and int.MaxValue - 1. Public APIs should enforce a much smaller maximum of their own.
Result Shape
page.Items
page.StartCursor
page.EndCursor
page.HasNextPage
page.HasPreviousPage
Use page.Map(...) to convert the items and keep the cursors. CursorPage<T>.Empty is a page without items.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Zift (>= 0.9.0)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.10)
- Zift (>= 0.9.0)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Zift (>= 0.9.0)
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 |
|---|---|---|
| 0.11.0 | 93 | 10/29/2026 |
| 0.10.0 | 97 | 10/26/2026 |
| 0.9.1 | 99 | 10/24/2026 |
| 0.9.0 | 93 | 10/24/2026 |
| 0.8.0 | 177 | 1/8/2026 |
| 0.7.0 | 153 | 1/4/2026 |
| 0.6.3 | 218 | 10/12/2025 |
| 0.6.2 | 214 | 10/5/2025 |
| 0.6.1 | 298 | 6/8/2025 |
| 0.6.0 | 223 | 5/11/2025 |
| 0.5.0 | 176 | 5/9/2025 |
| 0.4.1 | 226 | 5/4/2025 |
| 0.4.0 | 184 | 5/4/2025 |
| 0.3.1 | 200 | 5/2/2025 |
| 0.3.0 | 219 | 4/30/2025 |