π§ Introduction
In modern APIs, users expect features like filtering, sorting, and pagination on list endpoints. Manually writing this logic for every query can get messy fast—especially when your models evolve.
Enter Sieve — a lightweight, declarative, and extensible NuGet package that lets you offload that complexity. Whether you're using Entity Framework, Dapper, or even in-memory data, Sieve handles query logic dynamically based on query string inputs.
In this article, we’ll:
-
β
Build a real-world API using in-memory data
-
β
Use SieveModel
to drive dynamic query handling
-
β
Add custom filters (like IsAdult==true
)
-
β
Organize everything in a clean, scalable structure
Filtering, Sorting & Pagination with Sieve
π Step 1: Setup the Demo API
Create a new .NET Web API project.
dotnet new webapi -n SieveInMemoryDemo
cd SieveInMemoryDemo
dotnet add package Sieve
π Project Structure: Organizing Your Sieve API
Keeping your code organized is just as important as functionality. Here’s a clean project layout we’ll use:
SieveInMemoryDemo/
βββ Controllers/
β βββ UsersController.cs
βββ Models/
β βββ User.cs
βββ Filters/
β βββ CustomSieveFilters.cs
βββ Data/
β βββ DataStore.cs
βββ Program.cs
βββ SieveInMemoryDemo.csproj
βββ appsettings.json
This separation ensures:
-
Clean controllers (or minimal endpoints)
-
Easy extension for EF Core or real DB
-
Flexible and testable components
π§© Step 2: Create the User
Model
using Sieve.Attributes;
public class User
{
public int Id { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public string Name { get; set; }
[Sieve(CanFilter = true, CanSort = true)]
public int Age { get; set; }
[Sieve(CanSort = true)]
public DateTime CreatedAt { get; set; }
}
ποΈ Step 3: Create the In-Memory Data Store
public class DataStore
{
public List<User> Users { get; } = new()
{
new User { Id = 1, Name = "Alice", Age = 28, CreatedAt = DateTime.Now.AddDays(-3) },
new User { Id = 2, Name = "Bob", Age = 35, CreatedAt = DateTime.Now.AddDays(-10) },
new User { Id = 3, Name = "Charlie", Age = 22, CreatedAt = DateTime.Now.AddDays(-1) },
new User { Id = 4, Name = "Diana", Age = 40, CreatedAt = DateTime.Now.AddDays(-5) },
new User { Id = 5, Name = "Eve", Age = 30, CreatedAt = DateTime.Now }
};
}
π Step 3: Add a Custom Filter (Optional)
using Sieve.Services;
public class CustomSieveFilters : ISieveCustomFilterMethods
{
public IQueryable<User> IsAdult(IQueryable<User> source, string op, string value)
{
return value == "true"
? source.Where(u => u.Age >= 18)
: source.Where(u => u.Age < 18);
}
}
π§ͺ Step 4: Create UsersController
using Microsoft.AspNetCore.Mvc;
using Sieve.Models;
using Sieve.Services;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ISieveProcessor _sieveProcessor;
private readonly DataStore _dataStore;
public UsersController(ISieveProcessor sieveProcessor, DataStore dataStore)
{
_sieveProcessor = sieveProcessor;
_dataStore = dataStore;
}
[HttpGet]
public IActionResult GetUsers([FromQuery] SieveModel sieveModel)
{
var users = _dataStore.Users.AsQueryable();
var result = _sieveProcessor.Apply(sieveModel, users);
return Ok(result);
}
}
βοΈ Step 5: Configure DI in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register Sieve + In-Memory Store
builder.Services.AddControllers();
builder.Services.AddScoped<ISieveProcessor, SieveProcessor>();
builder.Services.AddScoped<ISieveCustomFilterMethods, CustomSieveFilters>();
builder.Services.AddSingleton<DataStore>();
var app = builder.Build();
app.MapControllers(); // enables attribute routing
app.Run();
π Example URLs to Test
/api/users?filters=Age>25&sorts=-CreatedAt&page=1&pageSize=2
/api/users?filters=Name@=a
/api/users?filters=IsAdult==true
/api/users?sorts=Age
π What is SieveModel
?
SieveModel
is a built-in class that maps query string parameters like filters
, sorts
, and pagination into a model you can use in your controller:
public class SieveModel
{
public string? Filters { get; set; }
public string? Sorts { get; set; }
public int? Page { get; set; }
public int? PageSize { get; set; }
}
Sieve uses this model behind the scenes to construct a dynamic LINQ expression.
β
Swagger Screenshot Section
https://localhost:7107/api/Users?Filters=age>35
![]()
https://localhost:7107/api/Users?Filters=name==Alice
![]()
β
Supported Filter Operators in Sieve
Operator |
Meaning |
== |
Equals |
!= |
Not equals |
> |
Greater than |
< |
Less than |
>= |
Greater than or equal |
<= |
Less than or equal |
@= |
Contains (like %x% ) |
!@= |
Does not contain |
β
Conclusion
Sieve takes the pain out of writing repetitive filtering, sorting, and paging logic. With the SieveModel
, attribute-based configuration, and optional custom filters, you can give users powerful query features with minimal effort.
By using a clean controller-based structure, your code is not only easier to test and scale, but also production-ready.
π― If you're building APIs in .NET, Sieve deserves a spot in your toolbox.
Thanks
Naimish Makwana