Zift is a lightweight and extensible library for query composition over IQueryable sources.
It supports dynamic filtering, sorting, and pagination, and is designed to be easily extended with custom query criteria.
Designed to work seamlessly with Entity Framework Core and any LINQ-compatible data source, Zift enables both runtime-defined querying (e.g., from API parameters) and compile-time query construction through fluent builders.
- Dynamic Filtering — Parse string-based filter expressions into type-safe LINQ queries.
- Predicate-Based Filtering — Define custom filter criteria using expressions.
- Fluent Sorting — Compose multi-level sorts dynamically or fluently in code.
- Dynamic Sorting — Parse string-based sort clauses like
"Name desc, Price asc". - Pagination — Apply paging to queries and return paginated result sets with metadata.
- IQueryable Extensions — Integrate filtering, sorting, and pagination directly over any
IQueryable<T>. - Entity Framework Core Support — Async pagination extensions with cancellation support.
- Extensible Design — Implement custom filter, sort, and pagination criteria types when needed.
- Target Framework: .NET 8
- Dependencies:
- Zift has no external dependencies.
- Zift.EntityFrameworkCore depends on Entity Framework Core 8.x.
Install the core Zift package:
dotnet add package ZiftIf you're using Entity Framework Core and need async pagination support, install the EF Core integration package instead:
dotnet add package Zift.EntityFrameworkCoreThe integration package includes the core package as a dependency — no need to install both.
Alternatively, if you prefer to work with the source:
- Clone the repository locally.
- Reference the project(s) directly from your solution.
var products = await dbContext.Products
.Filter(new DynamicFilterCriteria<Product>("Price > 1000 && Manufacturer == 'Logitech'"))
.SortBy(sort => sort.Descending(p => p.Price))
.ToPaginatedListAsync(pageNumber: 1, pageSize: 20);- Filter products where price is greater than 1000 and manufacturer is "Logitech".
- Sort descending by price.
- Paginate to return 20 items starting from page 1.
At the core of Zift is the ICriteria<T> interface, which defines a simple contract for applying transformations over IQueryable<T> sources:
public interface ICriteria<T>
{
IQueryable<T> ApplyTo(IQueryable<T> query);
}All Zift functionality — filtering, sorting, and pagination — builds upon this common foundation.
| Interface | Purpose |
|---|---|
IFilterCriteria<T> |
Defines a filtering operation (e.g., applying .Where()). |
ISortCriteria<T> |
Defines a sorting operation (e.g., applying .OrderBy() / .ThenBy()). |
IPaginationCriteria<T> |
Defines a pagination operation (e.g., applying .Skip() / .Take()). |
Each criteria type offers default implementations and supports both strongly-typed and dynamic variants for runtime-defined queries.
Zift provides a set of IQueryable extensions (Filter, SortBy, ToPaginatedList) that allow queries to be composed fluently for filtering, sorting, and pagination:
var query = dbContext.Products
.Filter(new DynamicFilterCriteria<Product>("Rating >= 4"))
.SortBy(sort => sort.Ascending(p => p.Name))
.ToPaginatedList(pageSize: 25);Queries are composed using standard LINQ patterns and executed only when enumerated.
- Unified
ICriteria<T>abstraction. - Deferred query execution.
- Null-safe expression handling.
- Lightweight, extensible core.
- Persistence-agnostic — no repository or unit-of-work assumptions.
Filtering in Zift is based on the IFilterCriteria<T> interface.
An IFilterCriteria<T> applies a filtering operation over an IQueryable<T>, typically by adding a .Where() clause.
You can filter using:
- A custom criteria object
- A LINQ predicate
- A dynamic string expression
Use PredicateFilterCriteria<T> (or a direct predicate) to apply LINQ-style filters:
var expensiveProducts = dbContext.Products
.Filter(new PredicateFilterCriteria<Product>(p => p.Price > 1000));
// Or directly with a lambda expression
var expensiveProducts = dbContext.Products
.Filter(p => p.Price > 1000);You can implement IFilterCriteria<T> to create reusable, strongly-typed filters:
public class PreferredCustomerFilter : IFilterCriteria<User>
{
public IQueryable<User> ApplyTo(IQueryable<User> query)
{
var preferredSince = DateTime.UtcNow.AddYears(-2); // Registered at least 2 years ago
return query.Where(u => u.RegistrationDate <= preferredSince);
}
}Usage:
var preferredCustomers = dbContext.Users
.Filter(new PreferredCustomerFilter());Use DynamicFilterCriteria<T> (or a plain string) to apply dynamic filters:
var filteredCategories = dbContext.Categories
.Filter(new DynamicFilterCriteria<Category>("Name ^= 'Gaming' && Products:count > 0"));
// Or directly with a string expression
var filteredCategories = dbContext.Categories
.Filter("Name ^= 'Gaming' && Products:count > 0");Null-safe expression handling can be enabled via configuration to prevent errors on missing properties or null collections.
Supports:
- Scalar comparisons (
==,>,<, etc.) - Membership comparisons with
in(e.g.,Name in ['Laptop', 'Smartphone']) - Case-insensitive string matching with
:i(e.g.,Name ==:i 'laptop') - Nested properties (e.g.,
Products.Manufacturer) - Collection quantifiers (
:any,:all) and projections (:count) - Logical operators (
&&,||,!())
Example:
// Find products where (price > 1000 OR name contains "Pro") AND at least one review has rating >= 4
var products = dbContext.Products
.Filter(new DynamicFilterCriteria<Product>(
"(Price > 1000 || Name %= 'Pro') && Reviews.Rating >= 4"));For a more comprehensive reference on dynamic filtering expressions, see the Dynamic Filtering Documentation.
Sorting in Zift is based on the ISortCriteria<T> interface, supporting multiple sort operations applied in order.
Sort using the fluent SortCriteriaBuilder<T>:
var sortedProducts = dbContext.Products
.SortBy(sort => sort
.Descending(p => p.Price)
.Ascending(p => p.Name));var sortedProducts = dbContext.Products
.SortBy(sort => sort
.Ascending("Name")
.Descending("Price"));var sortCriteria = new SortCriteria<Product>();
sortCriteria.Add(new SortCriterion<Product, decimal>(p => p.Price, SortDirection.Descending));
sortCriteria.Add(new SortCriterion<Product>("Name", SortDirection.Ascending));
var sortedProducts = dbContext.Products
.SortBy(sortCriteria);Sort using a SQL-style ORDER BY string:
var sortedProducts = dbContext.Products
.SortBy(sort => sort.Clause("Price DESC, Name ASC"));Default sort direction is ascending if omitted.
You can also specify a custom ISortDirectiveParser<T> implementation if you need different parsing behavior:
var parser = new CustomSortDirectiveParser<Product>();
var sortedProducts = dbContext.Products
.SortBy(sort => sort.Clause("Price DESC, Name ASC", parser));Pagination in Zift is based on IPaginationCriteria<T>, applying .Skip() and .Take() operations.
Use ToPaginatedList or ToPaginatedListAsync to apply paging by specifying the page number and page size:
var paginatedProducts = await dbContext.Products
.ToPaginatedList(pageNumber: 1, pageSize: 20);Or use the async version with Entity Framework Core:
var paginatedProducts = await dbContext.Products
.ToPaginatedListAsync(pageNumber: 1, pageSize: 20, cancellationToken);Both methods support default parameters:
pageNumberdefaults to 1pageSizedefaults toPaginationCriteria<T>.DefaultPageSize
Alternatively, manually construct a PaginationCriteria<T> object and pass it in:
var paginationCriteria = new PaginationCriteria<Product> { PageNumber = 1, PageSize = 20 };
var paginatedProducts = await dbContext.Products
.ToPaginatedListAsync(paginationCriteria);Paginated results implement IPaginatedList<T>, which exposes:
PageNumberPageSizePageCountTotalCount
Example:
if (paginatedProducts.HasNextPage())
{
// Load next page...
}Zift extends IQueryable<T> with:
| Method | Purpose |
|---|---|
Filter(IFilterCriteria<T>) |
Apply filtering using a criteria object. |
Filter(Expression<Func<T, bool>>) |
Apply filtering using a predicate. |
Filter(string) |
Apply filtering using a dynamic expression. |
SortBy(ISortCriteria<T>) |
Apply sorting using a criteria object. |
SortBy(Action<SortCriteriaBuilder<T>>) |
Apply sorting using a fluent builder. |
ToPaginatedList(IPaginationCriteria<T>) |
Apply pagination using a criteria object. |
ToPaginatedList(int, int) |
Apply pagination using page number and page size. |
ToPaginatedListAsync(IPaginationCriteria<T>) |
Apply pagination using a criteria object (EF Core). |
ToPaginatedListAsync(int, int) |
Apply pagination using page number and page size (EF Core). |
Create custom filtering, sorting, or pagination criteria by implementing the respective interfaces.
Example custom filter:
public class ActiveReviewFilter : IFilterCriteria<Review>
{
public IQueryable<Review> ApplyTo(IQueryable<Review> query)
{
return query.Where(r => r.DatePosted != null);
}
}Zift is under active development and not yet considered stable.
Potential future enhancements:
- Expanded Modifier Support: Extend string modifiers (currently only
:i) and collection projections (currently only:count). - Cursor-Based Pagination: Look into supporting keyset-style pagination.
Zift is licensed under the MIT License.
