Использование параметров запроса для привязки модели. Постраничный вывод результатов

В предыдущей части были рассмотрены общие моменты, касающиеся передачи данных в контроллеры через параметры запросов. Однако, как говорят, теория без практики мертва, практика без теории слепа. В этой части и далее мы рассмотрим несколько практических примеров использования параметров запросов в приложениях Web API и начнем с одного из важнейших вопросов — постраничного вывода результатов.

Постраничная отправка ресурсов API или пэйджинг (от англ. paging) представляет собой отправку данных пользователю частями (страницами). Наиболее часто, этот способ повышения производительности приложения организуется путем передачи в контроллер данных через параметры запроса. Например, запрос с разбитием на страницы может выглядеть следующим образом:

https://localhost?current_page=1&last_page=12718&per_page=30

Здесь определено три параметра:

  • current_page — текущая страница на которой находится пользователь
  • last_page — последняя страница с результатами
  • per_page — количество элементов на одной странице

Чтобы продемонстрировать реализацию разбиения результатов запроса на страницы, воспользуемся шаблонным приложением ASP.NET Core Web API на основе контроллеров. Для начала, немного изменим класс WeatherForecast:

public class WeatherForecast
{
    public int Id { get; set; }
    public DateOnly Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}

а также, изменим количество возвращаемых объектов в методе Get() контроллера:

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 500).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

Теперь контроллер на каждый запрос отправляет 500 объектов. Нетрудно догадаться, что в реальном приложении количество возвращаемых результатов может достигать тысяч и десятков тысяц объектов одного типа и не использовать в этом случае пейджинг — смерти подобно. Первое, что нам необходимо сделать — это определиться с тем, как в приложении будут передаваться данные о параметрах разбиения результатов. Это необходимо сделать, как минимум для того, чтобы в дальнейшем использовать один и тот же формат данных для различных сущностей в приложении. Добавим в проект новую папку Shared и разместим в ней абстрактный класс следующего содержания:

public abstract class RequestParameters
{
    public const int MAX_PAGE_SIZE = 100;
    private int _pageSize = 50;
    public int PageNumber { get; set; } = 1;
    public int PageSize
    {
        get
        {
            return _pageSize;
        }
        set
        {
            _pageSize = value <= MAX_PAGE_SIZE ? value : MAX_PAGE_SIZE;
        }
    }
}

Этот класс содержит два свойства: PageSize, определяющее размер одной страницы данных и PageNumber – номер текущей страницы данных. При этом мы ограничиваем максимально возможный размер ста элементами. Теперь, чтобы организовать постраничную работу с каким-либо ресурсом в нашем приложении, нам необходимо создать новый класс, который будет наследоваться от RequestParameters и определять параметры разбиения списка конкретных ресурсов. Например, создадим новый класс для определения параметров постраничного вывода прогноза погоды. Добавим в папку Shared еще один класс:

public class ForecastParameters: RequestParameters
{
}

Задействуем этот класс в нашем приложении. Изменим метод Get() контроллера следующим образом:

[HttpGet]
public IEnumerable<WeatherForecast> Get([FromQuery] ForecastParameters parameters)
{
    var data = Enumerable.Range(1, 500).Select(index => new WeatherForecast
    {
        Id = index,
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    });

    var result = data.OrderBy(o => o.Id)
                     .Skip((parameters.PageNumber - 1) * parameters.PageSize)
                     .Take(parameters.PageSize).ToList();


    return result;
}

Здесь мы определили для параметра ForecastParameters указываем атрибут [FromQuery], изменяя тем самым приоритет источников привязки модели – мы ожидаем, что этот объект будет получен из параметров запроса. Далее формируется последовательность из 500 элементов WeatherForecast. Для того, чтобы было удобнее отслеживать, что возвращает действие контроллера, каждому элементу последовательности присваивается идентификатор Id. А дальше мы используем методы расширения LINQ для того, чтобы получить требуемую выборку элементов:

var result = data.OrderBy(o => o.Id)
                 .Skip((parameters.PageNumber - 1) * parameters.PageSize)
                 .Take(parameters.PageSize).ToList();

У нас в последовательности содержится 500 элементов, и допустим,  мы хотим получить весь этот список постранично по 15 элементов на странице (PageSize = 15). В этом случае выдача результатов будет происходить следующим образом:

  • получаем отсортированную по Id последовательность (в принципе, для примера мы могли бы обойтись и без этого метода расширения, но для порядка — пусть будет)
  • 1 страница (PageNumber = 1): пропускаем первые (1-1)*15 = 0 элементов и получаем 15 элементов. То есть на выходе мы получаем первые 15 элементов в списке;
  • 2 страница (PageNumber = 2): пропускаем первые (2-1)*15 = 15 элементов и получаем 15 элементов;
  • и т.д.
  • 7 страница: (PageNumber = 7: пропускаем первые (7-1)*15 = 90 элементов и получаем еще 15 элементов.

Несмотря на то, что в параметрах запроса указано, что необходимо возвращать по 15 элементов, вызов метода расширения LINQ Take() вернет только оставшиеся элементы списка и не вызовет никаких исключений, если их количества не хватает для возврата заданного набора.

Чтобы передать объект в строке параметров запроса необходимо перечислить свойства объекта как обычные пары «ключ-значение». Добавим в http-файл проекта следующий запрос:

GET {{WebApplication5_HostAddress}}/weatherforecast?pageSize=15&pageNumber=3
Accept: application/json

То есть попытаемся получить третью страницу результатов (начиная с 31 элемента) с пятнадцатью объектами. Вот как будет выглядеть результат выполнения этого запроса:

Для того, чтобы получить очередную страницу с результатами, нам будет необходимо изменять в запросе параметр PageNumber. Представленный пример работы вполне работоспособный и может использоваться в приложениях Web API для постраничной выдачи ресурсов, однако имеет один недостаток. Попробуем ответить на вопрос: как пользователь API узнает сколько страниц ещё осталось запросить, чтобы получить все данные? Очевидно, что пользователь должен будет выполнять запросы до тех пор, пока сервер не вернет код статуса HTTP 204 No Content (который, кстати, у нас даже не возвращается) или пустой набор данных. Будет намного лучше и информативнее для клиента, если мы будем предоставлять ему всю исчерпывающую информацию о выполнении запроса непосредственно в ответе сервера. Давайте немного улучшим наше приложение.

Чтобы возвращать в ответе сервера всю исчерпывающую информацию о запросе, создадим ещё один класс в папке Shared:

public class PagedList<T>
{
    public List<T> Data { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }

    public bool HasPrevious => CurrentPage > 1;
    public bool HasNext => CurrentPage < TotalPages;

    public PagedList(IEnumerable<T> list, int count, int pageSize, int pageNumber)
    {
        Data = [];
        TotalCount = count;
        PageSize = pageSize;
        CurrentPage = pageNumber;
        TotalPages = (int)Math.Ceiling(count / (double)pageSize);
        Data.AddRange(list);
    }

    public static PagedList<T> ToPagedList(IEnumerable<T> list, int pageNumber, int pageSize)
    {
        var items = list.Skip((pageNumber - 1) * pageSize).Take(pageSize);
        return new PagedList<T>(items, list.Count(), pageSize, pageNumber);
    }
}

Универсальный класс PagedList<T> представляет собой «усовершенствованный» список ресурсов типа T. У этого класса определены следующие полезные для пользователя свойства:

  1. CurrentPage – номер текущей страницы результатов.
  2. TotalPages – общее количество страниц с результатами.
  3. TotalCount – общее количество элементов доступных для получения.
  4. PageSize – количество элементов на одной странице.
  5. HasPrevious – возвращает true, если доступна загрузка предыдущей страницы с результатами.
  6. HasNext – возвращает true, если доступна загрузка следующей страницы с результатами.
  7. Data – список полученных результатов

Теперь посмотрим на конструктор этого класса:

public PagedList(IEnumerable<T> list, int count, int pageSize, int pageNumber)
{
    Data = [];
    TotalCount = count;
    PageSize = pageSize;
    CurrentPage = pageNumber;
    TotalPages = (int)Math.Ceiling(count / (double)pageSize);
    Data.AddRange(list);
}

В качестве параметров конструктора выступают:

  • IEnumerable<T> list – список ресурсов, которые должны быть отправлены пользователю
  • int count – общее количество ресурсов в базе данных
  • int pageSize, int pageNumber – параметры постраничной выдачи ресурсов

В конструкторе мы присваиваем свойствам класса их значения и рассчитываем общее количество страниц, которые пользователь может запросить (свойство TotalPages).

Для упрощения использования нового класса, у него также определен статичный метод:

public static PagedList<T> ToPagedList(IEnumerable<T> list, int pageNumber, int pageSize)
{
    var items = list.Skip((pageNumber - 1) * pageSize).Take(pageSize);
    return new PagedList<T>(items, list.Count(), pageSize, pageNumber);
}

Здесь стоит обратить внимание на первый параметр метода (list) – это список всех ресурсов в базе данных. Используя этот список, в методе создается уже объект типа PagedList<T>. Таким образом, используя этот класс для формирования ответа сервера, мы можем предоставить пользователю всю исчерпывающую информацию о выполнении запроса, включая информацию о том, имеются ли ещё неполученные ресурсы на сервере, сколько страниц с результатами можно получить при использовании заданных параметров и так далее. Воспользуемся этим классом в нашем приложении. Изменим тип возвращаемого результата и, соответственно, реализацию метода Get()

[HttpGet]
public ActionResult<PagedList<WeatherForecast>> Get([FromQuery] ForecastParameters parameters)
{
    var data = Enumerable.Range(1, 500).Select(index => new WeatherForecast
    {
        Id = index,
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    });

    var result = PagedList<WeatherForecast>.ToPagedList(data, parameters.PageNumber, parameters.PageSize);
    if (result.Data.Count > 0)
    {
        return result;
    }
    else
        return NoContent();
}

Чтобы обеспечить возврат пользователю кода 204 No Content, в случае, если список результатов будет пуст, мы изменили тип возвращаемого результата на ActionResult<PagedList<WeatherForecast>>. В теле самого метода мы формируем новый объект типа PagedList<WeatherForecast>, используя статический метод класса и возвращаем результаты пользователю:

var result = PagedList<WeatherForecast>.ToPagedList(data, parameters.PageNumber, parameters.PageSize); 
if (result.Data.Count > 0) 
{ 
   return result; 
} 
else return NoContent();

Для примера, запросим выдачу результатов по одному на странице. Вот как будет выглядеть результат выполнения запроса:

Если же мы запросим, например, 501-ю страницу, то получим вот такой ответ сервера:

Как можно видеть по рисунку, теперь мы можем непосредственно из ответа сервера узнать сколько всего элементов находится на сервере, на скольких страницах они могут размещаться, имеются ли ещё неполученные данные на сервере и так далее, а заодно и научились передавать в параметрах запроса целые объекты.

Итого

В этой части мы научились организовывать постраничную выдачу результатов запросов, используя параметры запроса. Показанный выше пример, конечно, может дорабатываться в зависимости от ваших потребностей, однако он дает представление о том, какую пользу можно извлечь из работы с параметрами запроса и то, как мы можем на практике воспользоваться передачей целых объектов через параметры запроса.

При подготовке этой публикации использовались материалы с сайта https://code-maze.com/

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии