TGViewer
Библиотека шарписта | C#, F#, .NET, ASP.NET Библиотека шарписта | C#, F#, .NET, ASP.NET @csharpproglib · 21.7K subscribers
Post #6589 3.46K
⚙️Паттерн пагинации и фильтрации через один запрос и нормальные DTO

Пагинация в API часто превращается либо в магию репозиториев, либо в жуткий копипаст. Лучше держать всё предельно простым: один запрос с фильтрами, подсчётом и проекцией в DTO.

Что вообще хотим от пагинации

Когда фронт просит список пользователей, ему нужно не только «20 штук с offset», а нормальные данные

• Текущая страница.
• Размер страницы.
• Список элементов.
• Общее количество записей под этими фильтрами.

Контейнер для любого списка: пользователей, заказов или логов.
public sealed record Paginated<T>(
IReadOnlyList<T> Items,
int Page,
int Size,
int Total
);

• Items то, что реально отображаем.
• Page номер текущей страницы.
• Size сколько элементов на странице.
• Total сколько записей всего под заданным фильтром.

Зачем DTO, а не сущность из EF

• EF сущность часто содержит поля, которые нельзя светить наружу (пароли, внутренние флаги, технические поля).

• DTO можно менять отдельно от внутренней модели база может жить своей жизнью, а API остаётся стабильным.

• EF Core умеет проецировать прямо в DTO через Select, не тянуть все поля сущности и не включать трекинг.

В итоге хендлер не выкидывает наружу сырые сущности, а отдаёт ровно то, что нужно клиенту.

Как выглядит запрос сверху
public sealed record SearchUsersQuery(
string? Q,
int Page = 1,
int Size = 20
) : IRequest<Paginated<UserSummary>>;

• Q — строка поиска.
• Page — номер страницы.
• Size — размер страницы.

Контроллер ничего не знает о базе и EF он просто пробрасывает запрос в Application слой:
[HttpGet]
public async Task<ActionResult<Paginated<UserSummary>>> Search(
[FromQuery] string? q,
[FromQuery] int page,
[FromQuery] int size,
ISender sender,
CancellationToken ct
)
{
var result = await sender.Send(new SearchUsersQuery(q, page, size), ct);
return Ok(result);
}


Что делает хендлер под капотом
public async Task<Paginated<UserSummary>> Handle(
SearchUsersQuery query,
CancellationToken ct)
{
var users = _db.Users.AsQueryable();

if (!string.IsNullOrWhiteSpace(query.Q))
{
var q = query.Q.Trim();
users = users.Where(u =>
u.Email.Contains(q) ||
u.Name.Contains(q));
}

var total = await users.CountAsync(ct);

var items = await users
.OrderBy(u => u.Email)
.Skip((query.Page - 1) * query.Size)
.Take(query.Size)
.AsNoTracking()
.Select(u => new UserSummary(
u.Id,
u.Email,
u.IsActive
))
.ToListAsync(ct);

return new Paginated<UserSummary>(
items,
query.Page,
query.Size,
total
);
}



• AsQueryable() чтобы можно было постепенно навешивать фильтры.

• Фильтрация по Q делается в базе, а не в памяти. Email и Name фильтруются прямо в SQL.

​• CountAsync считает Total для уже отфильтрованного набора, без Skip/Take. Это количество строк, которые удовлетворяют фильтрам.

• Skip и Take делают пагинацию на стороне БД через OFFSET / FETCH или аналог, а не в памяти приложения.

• AsNoTracking() говорит EF Core не отслеживать сущности в change tracker, что ускоряет чистые запросы на чтение.

• Select сразу проецирует в UserSummary EF не создаёт полноценные сущности, не подгружает лишние поля и не собирает сложные графы.

Всё это превращается в один адекватный SQL запрос, а не в серию SELECT * плюс ручная фильтрация и подсчёты.

Паттерн пагинации это не про сложность, а наоборот про простоту и предсказуемость. Отдельные DTO, контейнер Paginated<T>, один явный запрос с фильтрами, подсчётом и AsNoTracking() дают API, которое не врёт клиенту, хорошо масштабируется и остаётся читабельным через год.

📍 Навигация: Вакансии • Задачи • Собесы

🐸Библиотека шарписта

#sharp_view
  • 👍 30
  • ❤ 5
More from @csharpproglib
  1. Sep 28, 2026👨‍💻 Не показывайте технические ошибки пользователям Когда приложение падает с необработа…
  2. Sep 26, 2026📍 Навигация: Вакансии • Задачи • Собесы 🐸 Библиотека шарписта #garbage_collector
  3. Sep 25, 2026🔐 NuGet: Microsoft меняет сертификат подписи С 23 сентября Microsoft использует новый сер…
  4. Sep 24, 2026🤩 Как поймать зависший .NET-сервис Приложение начинает тормозить, запросы зависают, а в л…
  5. Sep 23, 2026⚙️ yield return не бесплатный Итераторы выглядят просто, но работают иначе: IEnumerable<in…
  6. Sep 22, 2026💡 Replace, Regex или StringBuilder? Для замены текста в C# есть несколько инструментов. И…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →