Код-ревью Middle EF Core обновлено

Код-ревью: эндпоинт заказов на EF Core

Вопрос

Эндпоинт возвращает последние заказы клиента с позициями. Найдите проблемы с производительностью, корректностью и безопасностью.

app.MapGet("/customers/{id}/orders", async (string id, AppDbContext db) =>
{
    var orders = db.Orders.ToList()
        .Where(o => o.CustomerId.ToString() == id)
        .OrderByDescending(o => o.CreatedAt);

    var result = new List<OrderDto>();
    foreach (var o in orders)
    {
        var items = db.OrderItems.Where(i => i.OrderId == o.Id).ToList();
        result.Add(new OrderDto(o.Id, o.Total, items.Count));
    }

    var raw = db.Orders.FromSqlRaw($"SELECT * FROM orders WHERE note LIKE '%{id}%'").ToList();
    return Results.Ok(new { result, raw });
});

Коротко

Главное в этом коде: работа с данными идёт в памяти, а не в базе. Ранний ToList() тянет всю таблицу, цикл с запросом позиций даёт N+1, а синхронные вызовы занимают потоки.

Что должен найти ревьюер

  1. Производительностьстрока 3
    ToList() до Where загружает все заказы всех клиентов в память, фильтр и сортировка идут в приложении. Фильтр должен уйти в SQL до материализации.
  2. Производительностьстрока 4
    Сравнение через ToString() не переведётся в SQL с индексом. Параметр надо типизировать (int id или Guid id) и сравнивать ключи напрямую.
  3. Производительностьстрока 10
    Запрос позиций в цикле: классическая проблема N+1. Нужен один запрос с проекцией Select(o => new OrderDto(o.Id, o.Total, o.Items.Count)).
  4. Производительностьстрока 3
    Чтение без изменений, но запросы отслеживаемые: лишние снимки в трекере. Для чтения AsNoTracking() или проекция в DTO.
  5. Ошибкастрока 5
    Нет ограничения выборки (Take), хотя нужны «последние» заказы: у старых клиентов ответ будет огромным.
  6. Безопасностьстрока 14
    FromSqlRaw с интерполяцией строки: пользовательский id попадает прямо в SQL, это SQL-инъекция. Нужен FromSql($"...") с параметром или LINQ EF.Functions.Like.
  7. Производительностьстрока 14
    Синхронные ToList() в async-обработчике блокируют поток пула: нужны ToListAsync() с CancellationToken.
  8. Безопасностьстрока 15
    Наружу отдаются сущности целиком (raw), включая служебные поля, а проверки, что клиент запрашивает свои заказы, нет.

Разбор

Главное в этом коде: работа с данными идёт в памяти, а не в базе. Ранний ToList() тянет всю таблицу, цикл с запросом позиций даёт N+1, а синхронные вызовы занимают потоки. Правильная версия одним запросом:

var result = await db.Orders.AsNoTracking()
    .Where(o => o.CustomerId == id)
    .OrderByDescending(o => o.CreatedAt)
    .Take(50)
    .Select(o => new OrderDto(o.Id, o.Total, o.Items.Count))
    .ToListAsync(ct);

Отдельно критична инъекция через FromSqlRaw с интерполированной строкой и отсутствие авторизации на чужие заказы.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.