Реализация нескольких версий API в ASP.NET

ASP.NET Core logoПри разработке API веб-приложения часто возникает вопрос создания и поддержки нескольких версий API. Один из самых простых способов сделать это – использовать библиотеку Microsoft.AspNetCore.Mvc.Versioning или ее версию для других платформ.

Формат номера версии API

Рассматриваемая библиотека поддерживает следующий формат номера версии:

[Version Group].[Major].[Minor]-[Status]

  • Version Group – группа API в формате даты YYYY-MM-DD.
  • Major – номер версии.
  • Minor – минорный номер версии.
  • Status – статус, например: Alpha, Beta, RC и т.д.

Каждая часть опциональная. То есть самый короткий номер версии может состоять только из любой одной части. Например, только значение Major.

Способы указания версии API

Клиентское приложение может указать используемую версию API следующими способами:

  • В параметре запроса (query string). Например: http://www.service.com/api/users?api-version=1.0
  • В сегменте адреса (URL). Пример: http://www.service.com/api/v1/users
  • В HTTP заголовке запроса (HTTP header).

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

По умолчанию значение ожидается в параметре запроса (с именем api-version) или сегменте URL. Для второго варианта потребуется дополнительная конфигурация Контроллеров приложения.

Добавление поддержки версионности API в проект

1. Установка библиотеки

В проект веб-приложения, в зависимости от используемой платформы, необходимо добавить один из следующих NuGet пакетов:

2. Добавление сервиса поддержки версионности API

Для включения поддержки версионности API в ASP.NET Core / ASP.NET 5 веб-приложении необходимо в Startup.ConfigureServices() добавить вызов AddApiVersioning():

public class Startup
{
    ...
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddApiVersioning();
    }

    ...
}

Для ASP.NET WebAPI веб-приложения вызов AddApiVersioning() будет расположен в WebApiConfig.Configuration().

У данного метода существует вариант с параметром ApiVersioningOptions opt. Он позволяет настроить поведение библиотеки так, как это требуется в конкретном приложении. Основные доступные опции будут рассмотрены ниже.

3. Создание нескольких версий API

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

4. Настройка Контроллеров

Необходимо указать какой версии API принадлежит тот или иной Контроллер. В зависимости от ожидаемого способа передачи номера версии API есть два варианта:

Номер версии API в параметре запроса или заголовке

Контроллерам следует добавить специальный атрибут с указанием номера версии:

[ApiVersion(<version>, <Depricated = false|true>)]

Параметры:

  • version – номер версии API.
  • Depricated – отмечает устаревшие версии API, которые скоро будут удалены. При этом в данный момент они по прежнему будут доступны для запросов. Данный параметр влияет на вывод списков поддерживаемых и устаревших версий API. 

Для одного Контроллера атрибут [ApiVersion] может быть указан несколько раз с различными номерами версий API. В этом случае Контроллер будет соответствовать нескольким заданным версиям API.

Номер версии API  в сегменте URL

В данном случае необходимо, кроме указания [ApiVersion], добавить переменную {version:apiVersion} в URL, указанный в атрибуте [Route]. Например:

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]

Пример

Допустим в приложении есть две версии API: 1.0 и 2.0. В каждой из них определена своя реализация Контроллера UsersController, которые должны быть доступны по одному и тому же URL. Ожидается что клиентское приложение передает номер версии API через параметр запроса или заголовок.

namespace VersionedApiApp.Api.V1.Controllers
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new[] { "User 1", "User 2" };
        }
    }
}

namespace VersionedApiApp.Api.V2.Controllers
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<User> Get()
        {
            return new[] { new User() { Id = 1, Name = "User 1" } };
        }
    }
}

Обратиться к созданным методам можно используя GET запрос по адресу
http://localhost:[port]/api/users/?api-version=[version]
, где [version] – номер версии: 1.0 или 2.0.

При использовании сегмента URL для передачи номера версии API в атрибут [Route] добавится переменная {version:apiVersion}:

namespace VersionedApiApp.Api.V1.Controllers
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/{version:apiVersion}/[controller]")]
    public class UsersController : ControllerBase
    {
        …
    }
}

namespace VersionedApiApp.Api.V2.Controllers
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/{version:apiVersion}/[controller]")]
    public class UsersController : ControllerBase
    {
        …
    }
}

5. Дополнительно: настройка Действий

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

[MapToApiVersion(<version>)]

Например, код выше можно переписать с использованием только одного Контроллера:

namespace VersionedApiApp.Controllers
{
    [ApiController]
    [ApiVersion("1.0")]
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        [HttpGet]
        [MapToApiVersion("1.0")]
        public IEnumerable<string> GetV1()
        {
            return new[] { "User 1", "User 2" };
        }

        [HttpGet]
        [MapToApiVersion("2.0")]
        public IEnumerable<User> GetV2()
        {
            return new[] { new User() { Id = 1, Name = "User 3" } };
        }

        [HttpDelete]
        public IActionResult Delete()
        {
            return this.Ok();
        }
    }
}

Обратите внимание, что:

  • Контроллер поддерживает версии API: 1.0 и 2.0
  • Действия GetV1() и GetV2() принадлежат различным версиям API.
  • У Действия Delete() отсутствует атрибут [MapToApiVersion]. Оно может быть вызвано для любой версии API, поддерживаемой данным Контроллером.

Кроме того, в коде Действия можно получить информацию о запрошенной версии API. Для этого существует метод расширение для HttpContext:

public static ApiVersion? GetRequestedApiVersion(this HttpContext context)

Еще один вариант получения этих данных – указать ApiVersion в качестве параметра Действия:

namespace VersionedApiApp.Api.V1.Controllers
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<string> Get(ApiVersion version)
        {
            …
        }
    }
}

6. Дополнительно: Контроллеры, независимые от версии API

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

[ApiVersionNeutral]

В этом случае Контроллер будет вызван даже если номер версии не указан.

Настройка поведения версионности API

Как было отмечено выше, у метода AddApiVersioning() существует перегрузка с параметром ApiVersioningOptions opt для настройки поведения поддержки версионности API:

public class Startup
{
    ...
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddApiVersioning(opt => {
            opt.DefaultApiVersion = new ApiVersion(1, 0);
            opt.AssumeDefaultVersionWhenUnspecified = true;
            opt.ReportApiVersions = true;
        });
    }

    ...
}

Рассмотрим некоторые опции, представленные свойствами класса ApiVersioningOptions.

Информация о поддерживаемых версиях API

Если свойство ApiVersioningOptions.ReportApiVersions будет установлено в значение true, то в HTTP ответ будут добавлены два следующих заголовка:

  • api-supported-versions – список поддерживаемых версий API.
  • api-deprecated-versions – перечень устаревших версий API (отмеченных свойством Depricated атрибута [ApiVersion]).

Получение номера версии

Свойство ApiVersioningOptions.ApiVersionReader содержит экземпляр класса, который реализует интерфейс IApiVersionReader и отвечает за получение номера версии API из запроса.

Библиотека содержит следующие реализации:

  • QueryStringApiVersionReader – значение в параметре строки запроса (query string).
  • UrlSegmentApiVersionReader – значение в сегменте URL.
  • HeaderApiVersionReader – значение в заданном HTTP заголовке запроса.
  • MediaTypeApiVersionReader – значение в HTTP заголовках Content-Type и Accept.
  • QueryStringOrHeaderApiVersionReader – значение в параметре строки запроса или HTTP заголовке.

Кроме того, можно объединять несколько классов при помощи статического метода ApiVersionReader.Combine(…).

Разработчик может расширить список поддерживаемых вариантов передачи номера версии API, реализовав IApiVersionReader для необходимых сценариев.

В качестве значения ApiVersionReader по-умолчанию используется комбинация из QueryStringApiVersionReader и UrlSegmentApiVersionReader.

Поведение при отсутствии в запросе версии API

Какая версия API будет выбрана при отсутствии указания в запросе, определяют три следующих свойства класса ApiVersioningOptions:

  • AssumeDefaultVersionWhenUnspecified – контролирует как будут обработаны запросы без указания версии
    • false – клиенту будет возвращена ошибка HTTP 400.
    • true – будет использован ApiVersionSelector для определения номера версии.
  • DefaultApiVersion – версия API по-умолчанию.
  • ApiVersionSelector – содержит реализацию IApiVersionSelector, которая определяет номер используемой версии. Варианты "из коробки":
    • DefaultApiVersionReader – будет использовано значение из свойства DefaultApiVersion. Данный класс используется по-умолчанию.
    • ConstantApiVersionReader – задает версию, которая указана в конструкторе этого класса.
    • CurrentImplementationApiVersionSelector – из списка поддерживаемых версий выбирает максимальную стабильную.
    • LowestImplementedApiVersionSelector – из списка поддерживаемых версий выбирает минимальную стабильную.

Для поддержки других сценариев необходимо реализовать IApiVersionSelector.

Возврат ошибки

Свойство ApiVersioningOptions.ErrorResponses содержит экземпляр класса, который реализует IErrorResponseProvider и отвечает за формирование ответа в случае ошибки.

Реализация по-умолчанию DefaultErrorResponseProvider возвращает код HTTP 400 и описание ошибки в следующих случаях:

  • ApiVersionUnspecified – не указана версия API.
  • UnsupportedApiVersion – указана неподдерживаемая версия API.
  • InvalidApiVersion – указана некорректная версия API (значение не смо��ло быть распознано).
  • AmbiguousApiVersion – разные версии API указаны одновременно несколькими способами (например в заголовке и в параметре строки запроса

Если свойство AssumeDefaultVersionWhenUnspecified равно false, то вместо возврата кода InvalidApiVersion будет использована версия API по-умолчанию.

Формат ответа об ошибке выглядит следующим образом:

{
    "error": {
        "code": "UnsupportedApiVersion",
        "message": "The HTTP resource that matches the request URI does not support the API version 3.",
        "innerError": null
    }
}

HTTP код ответа и формат можно изменить, реализовав собственный вариант IErrorResponseProvider.

Поддержка Swagger (документирование API)

И в завершении стоит отметить еще одну важную возможность библиотеки. Это поддержка нескольких версий API в Swagger. Для ее настройки необходимо добавить сервис Api Explorer вызовом метода AddVersionedApiExplorer(...). В результате код настройки для .NET Core / .NET 5 приложения будет выглядеть так:

public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    readonly IApiVersionDescriptionProvider provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>
        this.provider = provider;

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in provider.ApiVersionDescriptions) {
            options.SwaggerDoc(
                description.GroupName,
                new Info() {
                    Title = $"Sample API {description.ApiVersion}",
                    Version = description.ApiVersion.ToString(),
                });
        }
    }
}

…

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddApiVersioning();
    services.AddVersionedApiExplorer(opt => opt.GroupNameFormat = "'v'VVV");
    services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
    services.AddSwaggerGen();
}

…

public void Configure(IApplicationBuilder app, IApiVersionDescriptionProvider provider)
{
    app.UseMvc();
    app.UseSwagger();
    app.UseSwaggerUI(
    opt => {
        foreach (var description in provider.ApiVersionDescriptions) {
            opt.SwaggerEndpoint(
                $"/swagger/{description.GroupName}/swagger.json",
                description.GroupName.ToUpperInvariant());
        }
    });
}

Добавить комментарий