Содержание
При настройке системы маршрутизации, мы можем задавать шаблоны маршрутов, содержащих параметры и различные ограничения эти параметров. Сегодня более подробно разберемся с шаблонами маршрутов в ASP.NET Core Web API.
Параметры маршрута
Общие сведения о параметрах маршрутов в ASP.NET Core
Параметры маршрута указываются в фигурных скобках. Например, в шаблоне маршрута /api/weather/{day} сегмент {day} является параметром маршрута, означающим, что на место {day} должно подставляться любое значение (строка).
Параметры маршрутов активно используются при разработке различных Web API. Так, например, при проектировании API рекомендуется сосредотачиваться на сущностях с которыми мы работаем. При такой рекомендации маршруты к действиям контроллера могут соответствовать следующим шаблонам (на примере исполнителей и их задач) в зависимости от метода HTTP:
| Ресурс API | GET | POST | PUT | DELETE |
/performers |
Получение списка всех исполнителей | Создание нового исполнителя | Массовое обновление всех исполнителей | Массовое удаление всех исполнителей |
/performers/1 |
Получение информации по исполнителю 1 | Ошибка | Обновление информации по исполнителю 1 | Удаление исполнителя 1 |
/performers/1/tasks |
Задачи исполнителя 1 | Добавление новой задачи для исполнителя 1 | Массовое обновление всех задач для исполнителя 1 | Массовое удаление задач для исполнителя 1 |
В данном случае, для составления шаблона маршрутов, включающих в себя идентификаторы пользователей логично использование параметра. Например, мы могли бы использовать следующий шаблон маршрута для получения всех задач конкретного исполнителя:
/performers/{id}/tasks
и, при вызове методов API, подставлять на место параметра конкретные идентификаторы пользователей:
/performers/1/tasks или /performers/abcde/tasks
При этом, ASP.NET Core позволяет указывать в шаблонах маршрутов несколько параметров, например:
/tasks/{id}/{format}
здесь {id} и {format} являются параметрами маршрута на место которых при вызове метода API должны подставляться любые строковые значения. Если несколько параметров указываются в одном сегменте пути, то между ними должен находится разделитель. Например,
/tasks/{id}-{format}
здесь {id} и {format}указываются в одном сегменте пути и поэтому между ними используется разделитель — символ -.
Если параметр является необязательным, то в конце имени указывается символ ?, например
/tasks/{id}/{format?}
В данном случае, при вызове метода API мы можем не указывать параметр format так как он является необязательным. Теперь рассмотрим несколько примеров использования параметров маршрутов в ASP.NET Core Web API
Использование параметров маршрутов в ASP.NET Core Web API
Рассмотрим применение параметров маршрутов на примере шаблона приложения ASP.NET Core Web API, но немного переделаем контроллер WeatherForecastController:
[ApiController]
[Route("[controller]")] //первый шаблон маршрута
public class WeatherForecastController : ControllerBase
{
List<WeatherForecast> weathers;
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
weathers = Enumerable.Range(1, 10).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)]
})
.ToList();
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return weathers;
}
}
Чтобы можно было запрашивать элементы типа WeatherForecast по их Id, коллекция этих элементов будет формироваться при создании контроллера:
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
weathers = Enumerable.Range(1, 10).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)]
})
.ToList();
}
а классу WeatherForecast было добавлено новое свойство:
public class WeatherForecast
{
public int Id { get; set; }
//здесь прочие свойства класса
}
Теперь добавим еще один метод Get() в котроллер:
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return weathers;
}
[HttpGet("{id}")]
public WeatherForecast Get(int id)
{
return weathers[id-1];
}
Get() не проводятся проверки параметра id. Будем считать, что пользователь понятливый и будет передавать в параметре только числа от 1 до 10. В реальных приложениях Web API проверками значений параметров пренебрегать не стоит Чтобы получить значения параметра маршрута в качестве параметра метода мы объявили в методе Get() параметр с тем же именем — id. Также, мы можем указать для параметра метода атрибут FromRoute, который указывает, что параметр метода должен быть получен из пути запроса:
[HttpGet("{id}")]
public WeatherForecast Get([FromRoute]int id)
{
return weathers[id-1];
}
В результате, мы получим два метода API:
/WeatherForecast— возвращает все элементы спискаList<WeatherForecast>/WeatherForecast/{id}— возвращает один элемент списка по егоId
Протестируем эти методы API, добавив в http-файл нашего проекта следующие запросы:
@WebApplication3_HostAddress = http://localhost:5225
GET {{WebApplication3_HostAddress}}/weatherforecast/
Accept: application/json
###
GET {{WebApplication3_HostAddress}}/weatherforecast/1
Выполнение первого запроса:
Выполнение второго запроса:
Необязательные параметры маршрута
Технически, указав параметр id необязательным, мы могли бы сделать один метод, например, так:
[HttpGet("{id?}")]
public IEnumerable<WeatherForecast> Get([FromRoute]int id = -1)
{
if (id < 0)
return weathers;
return weathers.Where(w => w.Id == id);
}
Но, при этом, стоит учитывать, что при использовании параметра id в пути запроса пользователь ожидает получить конкретную сущность (объект типа WeatherForecast), а получает коллекцию сущностей состоящую из одного элемента. Избежать такого поведения API можно, заменив тип возвращаемого результата на IActionResult:
[HttpGet("{id?}")]
public IActionResult Get([FromRoute]int id = -1)
{
if (id < 0)
return Ok(weathers);
return Ok(weathers.Where(w => w.Id == id).First());
}
Ограничения маршрутов
Ограничения маршрутов используются для разрешения неоднозначных маршрутов API. Чтобы понять в чём может заключаться неоднозначность маршрута, добавим в наш контроллер ещё одно действие:
//новое действие контроллера
[HttpGet("{summaries}")]
public IActionResult GetBySummaties(string summaries)
{
return Ok(weathers.Where(w => w.Summary.Equals(summaries)).First());
}
[HttpGet("{id}")]
public WeatherForecast Get(int id)
{
return weathers[id - 1];
}
Этот код не вызовет никаких ошибок ни при сборке проекта, ни при его запуске. Для нас тоже, вроде бы, всё логично — один метод принимает в качестве параметра строку, второй — целое число. Однако для ASP.NET Core эти два маршрута одинаковые так как на место обоих параметров мы можем подставлять любые значения. Попробуем выполнить любой из этих методов и получим ошибку следующего содержания:
Для разрешения такой неоднозначности и применяются ограничения маршрутов. Ограничение указывается через символ : после параметра. Так, чтобы однозначно указать ASP.NET Core какой тип значения мы ожидаем получить в параметре маршрута мы можем изменить наши методы следующим образом:
[HttpGet("{summaries:alpha}")]
public IActionResult GetBySummaties(string summaries)
{
return Ok(weathers.Where(w => w.Summary.Equals(summaries)).First());
}
[HttpGet("{id:int}")]
public WeatherForecast Get(int id)
{
return weathers[id - 1];
}
Так, для параметра summaries мы указали ограничение alpha, означающее, что значением параметра будет одна или несколько букв без учёта регистра. Для параметра id мы указали ограничение int — только целые числа. В результате мы разрешили неоднозначность двух маршрутов и теперь об метода API будут работать без ошибок.
ASP.NET Core предоставляет достаточно обширный круг возможных ограничений маршрута, каждое из которых в ASP.NET Core соответствует определенному классу:
| Ограничение | Пример | Примеры совпадений | Примечания. | Класс |
|---|---|---|---|---|
int |
{id:int} |
123456789, -123456789 | Соответствует любому целому числу | IntRouteConstraint |
bool |
{active:bool} |
true, FALSE | Соответствует true или false. Без учета регистра |
BoolRouteConstraint |
datetime |
{dob:datetime} |
2016-12-31, 2016-12-31 7:32pm | Соответствует допустимому значению DateTime для инвариантного языка и региональных параметров. |
DateTimeRouteConstraint |
decimal |
{price:decimal} |
49.99, -1,000.01 | Соответствует допустимому значению decimal для инвариантного языка и региональных параметров. |
DecimalRouteConstraint |
double |
{weight:double} |
1.234, -1,001.01e8 | Соответствует допустимому значению double для инвариантного языка и региональных параметров. |
DoubleRouteConstraint |
float |
{weight:float} |
1.234, -1,001.01e8 | Соответствует допустимому значению float для инвариантного языка и региональных параметров. |
FloatRouteConstraint |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 | Соответствует допустимому значению Guid |
GuidRouteConstraint |
long |
{ticks:long} |
123456789, -123456789 | Соответствует допустимому значению long |
Long |
minlength(value) |
{username:minlength(4)} |
Rick | Строка должна содержать не менее 4 символов | MinLengthRouteConstraint |
maxlength(value) |
{filename:maxlength(8)} |
MyFile | Строка должна содержать не более 8 символов | MaxLengthRouteConstraint |
length(length) |
{filename:length(12)} |
somefile.txt | Длина строки должна составлять ровно 12 символов | LengthRouteConstraint |
length(min,max) |
{filename:length(8,16)} |
somefile.txt | Строка должна содержать от 8 до 16 символов | LengthRouteConstraint |
min(value) |
{age:min(18)} |
19 | Целочисленное значение не меньше 18 | MinRouteConstraint |
max(value) |
{age:max(120)} |
91 | Целочисленное значение не больше 120 | MaxRouteConstraint |
range(min,max) |
{age:range(18,120)} |
91 | Целочисленное значение от 18 до 120 | RangeRouteConstraint |
alpha |
{name:alpha} |
John | Строка должна состоять из одной буквы или нескольких (a—z) без учета регистра. |
AlphaRouteConstraint |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 | Строка должна соответствовать регулярному выражению. См. советы по определению регулярного выражения. | RegexRouteConstraint |
required |
{name:required} |
John | Определяет обязательное наличие значения, не относящегося к параметру, во время формирования URL-адреса | RequiredRouteConstraint |
К одному и тому же параметру можно применять несколько ограничений. Для этого, все ограничения указываются через символ : после параметра, например:
[HttpGet("{id:int:min(1):max(10)}")]
Здесь мы применили к параметру сразу три ограничения — по типу (int), минимальное значение (min) и максимальное значение (max).
Следует также обратить внимание на рекомендации Microsoft относительно применения ограничений, а именно: не используйте ограничения для проверки входных данных. Эта рекомендация продиктована следующим моментом: при недопустимых значениях параметров сервер должен возвращать клиенту ошибку 400 BadRequest в то время как использование ограничения для проверки входных данных вернет ошибку 404 NotFound.
Проверить это достаточно просто, используя наш пример. Используем следующие ограничения параметр id:
[HttpGet("{id:int:min(1):max(10)}")]
public WeatherForecast Get(int id)
{
return weathers[id - 1];
}
и выполним запрос с id=11 непосредственно в браузере без использования интерфейса Swagger. В результате получим:
Поэтому ограничения min и max стоит убрать и каким-либо образом обработать возможный неверный ввод данных. Например, один из возможных вариантов обработки ввода неверных данных может выглядеть так:
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
if ((id < 1) || (id > 10))
return BadRequest();
return Ok(weathers[id - 1]);
}
Здесь мы используем вместо конкретного типа возвращаемого результата тип IActionResult, проверяем значение id и либо возвращаем результат, либо ошибку 400. В случае ошибки клиент API получит следующий объект:
что соответствует рекомендации — клиенту возвращается ошибка с кодом 400 «Bad request».
Итого
При разработке системы маршрутизации в проекте мы можем использовать в шаблонах маршрутов параметры, которые включаются в шаблон маршрута в фигурных скобках. Для разрешения неоднозначных маршрутов в ASP.NET Core применяются ограничения маршрутов, которые указываются через двоеточие после имени параметра.

