Zift.EntityFrameworkCore 0.9.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Zift.EntityFrameworkCore --version 0.9.1
                    
NuGet\Install-Package Zift.EntityFrameworkCore -Version 0.9.1
                    
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="Zift.EntityFrameworkCore" Version="0.9.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zift.EntityFrameworkCore" Version="0.9.1" />
                    
Directory.Packages.props
<PackageReference Include="Zift.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 Zift.EntityFrameworkCore --version 0.9.1
                    
#r "nuget: Zift.EntityFrameworkCore, 0.9.1"
                    
#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 Zift.EntityFrameworkCore@0.9.1
                    
#: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=Zift.EntityFrameworkCore&version=0.9.1
                    
Install as a Cake Addin
#tool nuget:?package=Zift.EntityFrameworkCore&version=0.9.1
                    
Install as a Cake Tool

Zift Logo

NuGet NuGet CI Build & Publish

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.Email

  • Logical operators &&, ||, !

    Example: Price > 100 && Price < 500

  • Grouping (...) for evaluation order

    Example: (Price > 100 && Price < 500) || IsFeatured == true

  • Comparison operators ==, !=, <, <=, >, >=

    Example: Rating >= 4

  • String operators %= (contains), ^= (starts with), $= (ends with)

    Example: Title %= "C#"

  • in operator with list literals

    Example: 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, and null.
  • 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 before AsCursorQuery() 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().

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 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. 
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
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