Фильтрация в ASP.NET Core Web Api

Что такое фильтрация?

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

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

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

Возьмем, к примеру, сайт по продаже автомобилей. При фильтрации автомобилей, которые вы хотите, в идеале вы должны выбрать:

  • Производитель автомобиля как категория из списка или раскрывающегося списка.
  • Модель автомобиля из списка или раскрывающегося списка.
  • Он новый или подержанный?
  • Город, в котором находится продавец, в раскрывающемся списке.
  • Цена автомобиля - это поле ввода (числовое)
  • ….

Таким образом, запрос будет выглядеть примерно так: https://bestcarswebsite.com/sale?manufacturer=ford&model=expedition&state=used&city=washington&price_from=30000&price_to=50000

Или даже так: https://bestcarswebsite.com/sale/filter?data[manufacturer]=ford&[model]=expedition&[state]=used&[city]=washington&[price_from]=30000&[price_to]=50000

Или что-нибудь еще, что имеет для вас наибольшее значение. API должен анализировать фильтр, поэтому нам не нужно слишком сильно с ним сходить :).

Теперь, когда мы знаем, что такое фильтрация, давайте посмотрим, чем она отличается от поиска.

Чем фильтрация отличается от поиска

При поиске результатов у вас обычно есть только один ввод, и он используется для поиска чего-либо на веб-сайте.

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

На нашем автомобильном веб-сайте мы использовали бы поле поиска, чтобы найти модель автомобиля «Ford Expedition», и получили бы все результаты, соответствующие названию автомобиля «Ford Expedition». Это вернет все имеющиеся автомобили Ford Expedition.

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

Это улучшает взаимодействие с пользователем.

Пример: https://bestcarswebsite.com/sale/search?name=ford focus

Использование поиска не означает, что мы не можем использовать вместе с ним фильтры. Имеет смысл использовать фильтрацию и поиск вместе, поэтому мы должны учитывать это при написании исходного кода.

Но хватит теории.

Давайте реализуем фильтры.

Как реализовать фильтрацию в веб-API ASP.NET Core

У нас есть объект DateOfBirth в нашем классе Owner. Предположим, мы хотим узнать, какие владельцы родились в период с 1975 по 1997 год. Мы также хотим иметь возможность указать только начальный год, а не конечный, и наоборот.

Нам понадобится такой запрос: https://localhost:5001/api/owner?minYearOfBirth=1975&maxYearOfBirth=1997

Но мы тоже хотим иметь возможность: https://localhost:5001/api/owner?minYearOfBirth=1975

Или вот так: https://localhost:5001/api/owner?maxYearOfBirth=1997

Хорошо, у нас есть спецификация. Посмотрим, как это реализовать.

Если вы ранее читали статью по добавлению пагинации в NET Core API, то вы можете использовать класс OwnerParameters, который мы использоваться для определения параметров запроса с разбиением на страницы.

Давайте расширим наш класс OwnerParameters для поддержки фильтрации:

public class OwnerParameters : QueryStringParameters
{
	public uint MinYearOfBirth { get; set; }
	public uint MaxYearOfBirth { get; set; } = (uint)DateTime.Now.Year;

	public bool ValidYearRange => MaxYearOfBirth > MinYearOfBirth;
}

Мы добавили два свойства типа unsigned int (чтобы избежать отрицательных значений года), MinYearOfBirth и MaxYearOfBirth.

Поскольку значение по умолчанию uint равно 0, нам не нужно его явно определять, в этом случае 0 допустим. Для свойства MaxYearOfBirth мы хотим установить текущий год. Если мы не получим его через параметры запроса, нам есть над чем поработать. Неважно, если кто-то установит 3053 год в параметрах, это не повлияет на результаты.

Мы также добавили простое свойство проверки - ValidYearRange.. Его цель - сообщить нам, действительно ли максимальный год больше минимального. Если это не так, мы хотим, чтобы пользователь API знал, что он/она делает что-то не так.

Итак, теперь, когда у нас есть параметры, мы можем расширить контроллер:

[HttpGet]
public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters)
{
	if (!ownerParameters.ValidYearRange)
	{
		return BadRequest("Max year of birth cannot be less than min year of birth");
	}

	var owners = _repository.Owner.GetOwners(ownerParameters);

	var metadata = new
	{
		owners.TotalCount,
		owners.PageSize,
		owners.CurrentPage,
		owners.TotalPages,
		owners.HasNext,
		owners.HasPrevious
	};

	Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));

	_logger.LogInfo($"Returned {owners.TotalCount} owners from database.");

	return Ok(owners);
}

Как видите, в этом нет ничего особенного, мы добавили нашу проверку и ответ BadRequest с коротким сообщением для пользователя API.

Перейдем к реализации в нашем классе OwnerRepository:

public PagedList<Owner> GetOwners(OwnerParameters ownerParameters)
{
	var owners = FindByCondition(o => o.DateOfBirth.Year >= ownerParameters.MinYearOfBirth &&
								o.DateOfBirth.Year <= ownerParameters.MaxYearOfBirth)
							.OrderBy(on => on.Name);

	return PagedList<Owner>.ToPagedList(owners,
		ownerParameters.PageNumber,
		ownerParameters.PageSize);
}

Собственно, и здесь реализация довольно проста.

Мы используем метод FindByCondition, чтобы найти всех владельцев с DateOfBirth между MaxYearOfBirth и MinYearOfBirth.

Отправка и проверка некоторых запросов

У нас есть 3 варианта использования для тестирования.

Для справки, это владельцы в нашей базе данных:

Сначала протестируем только параметр minYearOfBirth. https://localhost:5001/api/owner?minYearOfBirth=1975

В этом случае мы не должны видеть Анну Бош в наших результатах.

Второй тест должен быть просто maxYearOf Birth. https://localhost:5001/api/owner?maxYearOfBirth=1997

Теперь Ник Сомион не должен появляться среди наших владельцев.

И последний тест должен включать как minYearOfBirth, так и maxYearOfBirth. https://localhost:5001/api/owner?minYearOfBirth=1975&maxYearOfBirth=1997

В этом случае ни Анна, ни Ник не должны отображаться в результатах.

Мы также должны проверить, работает ли наша проверка: https://localhost:5001/api/owner?minYearOfBirth=1975&maxYearOfBirth=1974

Мы должны получить неверный запрос с сообщением о недопустимом диапазоне.

В довершение всего мы можем объединить фильтрацию с нашим текущим решением для разбивки на страницы. https://localhost:5001/api/owner?minYearOfBirth=1975&maxYearOfBirth=1997&pageSize=2&pageNumber=2

Сможете угадать, каков результат (подсказка: это один человек)? Если угадали, дайте нам знать в комментариях.

Вот и все, мы проверили все подходящие случаи.

Заключение

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

Из этой статьи мы узнали:

  • Что такое фильтрация
  • Чем это отличается от поиска
  • Как реализовать фильтрацию в ASP.NET Core
  • Протестировали нашу реализацию