.NET  

Filtering, Sorting & Pagination Made Easy in .NET with Sieve

🧭 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