Контроллеры ASP.NET Core Web API. Параметры и ограничения маршрутов

При настройке системы маршрутизации, мы можем задавать шаблоны маршрутов, содержащих параметры и различные ограничения эти параметров. Сегодня более подробно разберемся с шаблонами маршрутов в 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:

  1. /WeatherForecast — возвращает все элементы списка List<WeatherForecast>
  2. /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 эти два маршрута одинаковые так как на место обоих параметров мы можем подставлять любые значения. Попробуем выполнить любой из этих методов и получим ошибку следующего содержания:

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches: WebApplication1.Controllers.WeatherForecastController.Get  WebApplication1.Controllers.WeatherForecastController.GetBySummaties

Для разрешения такой неоднозначности и применяются ограничения маршрутов. Ограничение указывается через символ : после параметра. Так, чтобы однозначно указать 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 LongRouteConstraint
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 Строка должна состоять из одной буквы или нескольких (az) без учета регистра. 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 применяются ограничения маршрутов, которые указываются через двоеточие после имени параметра.

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