Вы находитесь на странице: 1из 15

Пейджинг в ASP.

NET Core Web API


Автор: Владимир Пеканац | 18 ноября 2019 | 0
В этой статье мы узнаем, как реализовать разбиение на страницы в ASP.NET Core Web API. Разбиение на страницы (разбиение на
страницы) является одной из наиболее важных концепций при создании API RESTful.
На самом деле, мы не хотим возвращать коллекцию всех ресурсов при запросах нашего API. Это может вызвать проблемы с
производительностью, и оно никоим образом не оптимизировано для общедоступных или частных API. Это может привести к
значительному замедлению работы и даже сбоям приложений в серьезных случаях.

Исходный код этой статьи можно найти в репозитории GitHub . Если вы хотите следовать этой статье, вы можете использовать стартовую
ветвь и, если вы хотите получить окончательное решение или если вы застряли, переключитесь на конечную ветвь .

ПРИМЕЧАНИЕ : некоторая степень предыдущих знаний необходима, чтобы следовать этой статье. Он в значительной степени опирается
на серии ASP.NET Core Web API на Code Maze, поэтому, если вы не уверены, как настроить базу данных или как работает базовая архитектура
, мы настоятельно рекомендуем вам пройти серию.

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

Итак, давайте посмотрим, о чем мы будем говорить в этой статье:

Что такое пейджинг?

Начальная реализация

Пейджинговая реализация

Тестирование решения

Улучшение решения

Давайте начнем.

Что такое пейджинг?


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

Мало того, что это будет крайне неэффективным способом возврата результатов, но это также может иметь разрушительные
последствия для самого приложения или оборудования, на котором оно работает . Кроме того, каждый клиент имеет ограниченные
ресурсы памяти, и ему необходимо ограничить количество отображаемых результатов.
Таким образом, нам нужен способ вернуть определенное количество результатов клиенту, чтобы избежать этих последствий.

Посмотрим, как мы можем это сделать.

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

В нашем случае мы имеем OwnerController все необходимые действия над Owner объектом.

Одно конкретное действие, которое выделяется и которое нам нужно изменить, - это GetOwners() действие:

GetAllOwners action pre-change C#


1 [HttpGet]
2 public IActionResult GetOwners()
3 {
4 var owners = _repository.Owner.GetAllOwners();
5
6 _logger.LogInfo($"Returned all owners from database.");
7
8 return Ok(owners);
9 }

Какие звонки GetOwners() из OwnerRepository :

OwnerRepository.cs C#
1 public IEnumerable<Owner> GetOwners()
2 {
3 return FindAll()
4 .OrderBy(ow => ow.Name);
5 }

Метод FindAll () - это просто метод из базового класса репозитория, который возвращает весь набор владельцев.
1 public IQueryable<T> FindAll()
2 {
3 return this.RepositoryContext.Set<T>();
4 }

Как видите, это простое действие, предназначенное для возврата всех владельцев из базы данных, упорядоченной по имени.

И это делает именно это.

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

В итоге мы получили бы очень длинный запрос, который возвращает МНОГО данных .

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

Итак, имея это в виду, давайте изменим этот метод для поддержки подкачки страниц.

Пейджинговая реализация
Имейте в виду, мы не хотим изменять базовую логику хранилища или реализовывать какую-либо бизнес-логику в контроллере.

То, что мы хотим достичь, это что-то вроде этого https://localhost:5001/api/owners?pageNumber=2&pageSize=2 . Это должно
вернуть второй набор из двух владельцев, которые мы имеем в нашей базе данных.

Мы также хотим ограничить наш API, чтобы не возвращать всех владельцев, даже если кто-то звонит
https://localhost:5001/api/owners .

Давайте начнем с изменения контроллера:


OwnerController cs C#
1 [HttpGet]
2 public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters)
3 {
4 var owners = _repository.Owner.GetOwners(ownerParameters);
5
6 _logger.LogInfo($"Returned {owners.Count()} owners from database.");
7
8 return Ok(owners);
9 }

Несколько вещей, чтобы принять к сведению здесь:

Мы вызываем GetOwners метод из OwnerRepository , который еще не существует, но скоро мы его реализуем

Мы используем, [FromQuery] чтобы указать, что мы будем использовать параметры запроса, чтобы определить, какую страницу
и сколько владельцев мы запрашиваем

OwnerParameters класс является контейнером для фактических параметров

Нам также нужно создать OwnerParameters класс, поскольку мы передаем его в качестве аргумента нашему контроллеру. Давайте
создадим его в папке Models проекта Entities:

OwnerParameters.cs C#
1 public class OwnerParameters
2 {
3 const int maxPageSize = 50;
4 public int PageNumber { get; set; } = 1;
5
6 private int _pageSize = 10;
7 public int PageSize
8 {
9 get
10 {
11 return _pageSize;
12 }
13 set
14 {
15 _pageSize = (value > maxPageSize) ? maxPageSize : value;
16 }
17 }
18 }

Мы используем константу, maxPageSize чтобы ограничить наш API максимум 50 владельцами. У нас есть два открытых свойства -
PageNumber и PageSize. Если вызывающий не установил, PageNumber будет установлен в 1, а PageSize в 10.

Теперь давайте реализуем самую важную часть, логику хранилища.

Нам нужно расширить GetOwners() метод в IOwnerRepository интерфейсе и в OwnerRepository классе:

IOwnerRepository.cs C#
1 public interface IOwnerRepository : IRepositoryBase<Owner>
2 {
3 IEnumerable<Owner> GetOwners(OwnerParameters ownerParameters);
4 Owner GetOwnerById(Guid ownerId);
5 OwnerExtended GetOwnerWithDetails(Guid ownerId);
6 void CreateOwner(Owner owner);
7 void UpdateOwner(Owner dbOwner, Owner owner);
8 void DeleteOwner(Owner owner);
9 }

И логика:

OwnerRepository,cs C#
1 public IEnumerable<Owner> GetOwners(OwnerParameters ownerParameters)
2 {
3 return FindAll()
4 .OrderBy(on => on.Name)
5 .Skip((ownerParameters.PageNumber - 1) * ownerParameters.PageSize)
6 .Take(ownerParameters.PageSize)
7 .ToList();
8 }

Хорошо, самый простой способ объяснить это на примере.


Скажем, нам нужно получить результаты для третьей страницы нашего веб-сайта, считая 20 как количество результатов, которые
мы хотим . Это означало бы, что мы хотим пропустить первые (( 3 - 1) * 20 ) = 40 результатов, а затем взять следующие 20 и вернуть их
вызывающей стороне.

Имеет ли это смысл?

Тестирование решения
Теперь в нашей базе данных только несколько владельцев, поэтому давайте попробуем что-то вроде этого:

https://localhost:5001/api/owners?pageNumber=2&pageSize=2

Это должно вернуть следующее подмножество владельцев:

Paged owners result JavaScript


1 [
2 {
3 "id": "66774006-2371-4d5b-8518-2177bcf3f73e",
4 "name": "Nick Somion",
5 "dateOfBirth": "1998-12-15T00:00:00",
6 "address": "North sunny address 102"
7 },
8 {
9 "id": "a3c1880c-674c-4d18-8f91-5d3608a2c937",
10 "name": "Sam Query",
11 "dateOfBirth": "1990-04-22T00:00:00",
12 "address": "91 Western Roads"
13 }
14 ]

Если это то, что вы получили, вы на правильном пути.

Теперь, что мы можем сделать, чтобы улучшить это решение?

Улучшение решения
Поскольку мы возвращаем вызывающей стороне только подмножество результатов, мы могли бы иметь PagedList вместо List .

PagedList будет наследовать от List класса и добавит еще немного к нему. Мы также можем переместить логику пропуска / принятия в
PagedList, так как это имеет больше смысла.

Давайте реализуем это.

Реализация класса PagedList


Мы не хотим, чтобы наша логика пропуска / принятия осуществлялась внутри нашего репозитория. Давайте создадим для него класс:

PagedList.cs C#
1 public class PagedList<T> : List<T>
2 {
3 public int CurrentPage { get; private set; }
4 public int TotalPages { get; private set; }
5 public int PageSize { get; private set; }
6 public int TotalCount { get; private set; }
7
8 public bool HasPrevious => CurrentPage > 1;
9 public bool HasNext => CurrentPage < TotalPages;
10
11 public PagedList(List<T> items, int count, int pageNumber, int pageSize)
12 {
13 TotalCount = count;
14 PageSize = pageSize;
15 CurrentPage = pageNumber;
16 TotalPages = (int)Math.Ceiling(count / (double)pageSize);
17
18 AddRange(items);
19 }
20
21 public static PagedList<T> ToPagedList(IEnumerable<T> source, int pageNumber, int pageSize)
22 {
23 var count = source.Count();
24 var items = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList();
25
26 return new PagedList<T>(items, count, pageNumber, pageSize);
27 }
28 }

Как видите, мы передали логику пропуска / принятия статическому методу внутри PagedList класса. Мы добавили еще несколько
свойств, которые пригодятся в качестве метаданных для нашего ответа.

HasPrevious Значение true, если CurrentPage больше 1, и HasNext рассчитывается, если CurrentPage меньше, чем общее
количество страниц. TotalPages рассчитывается также путем деления количества элементов на размер страницы и последующего
округления до большего числа, поскольку страница должна существовать, даже если на ней есть один элемент.

Теперь, когда мы это выяснили, давайте изменим наше OwnerRepository и OwnerController соответственно.

Во-первых, нам нужно изменить репо (не забудьте также изменить интерфейс):

OwnerRepository.cs C#
1 public PagedList<Owner> GetOwners(OwnerParameters ownerParameters)
2 {
3 return PagedList<Owner>.ToPagedList(FindAll().OrderBy(on => on.Name),
4 ownerParameters.PageNumber,
5 ownerParameters.PageSize);
6 }

И тогда контроллер:

OwnerController.cs C#
1 [HttpGet]
2 public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters)
3 {
4 var owners = _repository.Owner.GetOwners(ownerParameters);
5
6 var metadata = new
7 {
8 owners.TotalCount,
9 owners.PageSize,
owners.CurrentPage,
10
11 owners.TotalPages,
12 owners.HasNext,
13 owners.HasPrevious
};
14
15
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));
16
17
_logger.LogInfo($"Returned {owners.TotalCount} owners from database.");
18
19
return Ok(owners);
20
}
21

Теперь, если мы отправим тот же запрос, что и раньше https://localhost:5001/api/owners?pageNumber=2&pageSize=2 , мы


получим тот же точный результат:

Same request result JavaScript


1 [
2 {
3 "id": "f98e4d74-0f68-4aac-89fd-047f1aaca6b6",
4 "name": "Martin Miller",
5 "dateOfBirth": "1983-05-21T00:00:00",
6 "address": "3 Edgar Buildings"
7 },
8 {
9 "id": "66774006-2371-4d5b-8518-2177bcf3f73e",
10 "name": "Nick Somion",
11 "dateOfBirth": "1998-12-15T00:00:00",
12 "address": "North sunny address 102"
13 }
14 ]

Но теперь у нас есть дополнительная полезная информация в X-Pagination заголовке ответа:


Заголовки ответа

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

Есть еще одна вещь, которую мы можем сделать, чтобы сделать наше решение еще более общим. У нас есть OwnerParameters класс, но
что, если мы хотим использовать его в нашем AccountController ? Параметры, которые мы отправляем контроллеру аккаунта, могут
отличаться. Возможно, не для подкачки страниц, но позже мы отправим несколько различных параметров, и нам нужно разделить классы
параметров.

Посмотрим, как это улучшить.

Создание класса родительских параметров


Сначала давайте создадим абстрактный класс QueryStringParameters . Мы будем использовать этот класс для реализации взаимно
используемых функций для каждого класса параметров, который мы реализуем. А поскольку у нас есть OwnerController и
AccountController , значит, нам нужно создавать OwnerParameters и AccountParameters классы.

Давайте начнем с определения QueryStringParameters класса внутри папки Models проекта Entities:

QueryStringParameters.cs C#
1 public abstract class QueryStringParameters
2 {
3 const int maxPageSize = 50;
4 public int PageNumber { get; set; } = 1;
5
6 private int _pageSize = 10;
7 public int PageSize
8 {
9 get
10 {
11 return _pageSize;
12 }
13 set
14 {
15 _pageSize = (value > maxPageSize) ? maxPageSize : value;
16 }
17 }
18 }

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

Теперь нам нужно создать AccountParameters класс, а затем наследовать его в QueryStringParameters классах OwnerParameters и
AccountParameters.

Удалить логику OwnerParameters и наследовать QueryStringParameters :

OwnerParameters.cs C#
1 public class OwnerParameters : QueryStringParameters
2 {
3
4 }

И создайте AccountParameters класс внутри папки Models:

AccountParameters.cs C#
1 public class AccountParameters : QueryStringParameters
2 {
3
4 }
Теперь эти классы выглядят немного пустыми, но скоро мы наполним их другими полезными параметрами и посмотрим, какова реальная
выгода. Сейчас важно, чтобы у нас был способ отправить другой набор параметров для AccountController и OwnerController .

Теперь мы можем сделать что-то подобное и внутри нашего AccountController :

C#
1 [HttpGet]
2 public IActionResult GetAccountsForOwner(Guid ownerId, [FromQuery] AccountParameters parameters)
3 {
4 var accounts = _repository.Account.GetAccountsByOwner(ownerId, parameters);
5
6 var metadata = new
7 {
8 accounts.TotalCount,
9 accounts.PageSize,
10 accounts.CurrentPage,
11 accounts.TotalPages,
12 accounts.HasNext,
13 accounts.HasPrevious
14 };
15
16 Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));
17
18 _logger.LogInfo($"Returned {accounts.TotalCount} owners from database.");
19
20 return Ok(accounts);
21 }

И благодаря наследованию параметров подкачки через QueryStringParameters класс мы получаем то же самое поведение. Ухоженная.

Вот и все, давайте подведем итоги.

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

В этой статье мы рассмотрели:

Самый простой способ реализовать нумерацию страниц в ASP.NET Core Web API

Протестировано решение в реальном сценарии

Улучшил это решение, введя PagedList сущность и разделив наши параметры для разных контроллеров

Надеюсь, вам понравилась эта статья и вы узнали что-то новое или полезное из нее.

Если вам понравилось читать эту статью и вы хотели бы получать уведомления о недавно опубликованном контенте .NET Core, мы
призываем вас подпишитесь на наш блог.

Вам также может понравиться