ASP .NET Core -  Versionando sua Web API

 Hoje veremos como realizar o versionamento de uma Web API usando a ASP .NET Core no NET 5.0.

Como todo o produto de software as APIs também ficam desatualizadas ao longo do tempo e novos recursos deverão ser implementados criando assim uma nova versão da API.

Você deve ter em mente que ao publicar uma API e expor os serviços aos clientes você deverá ter muito cuidado ao atualizar a API pois se incluir novos recursos e desabilitar os recursos antigos pode deixar alguns dos clientes sem acesso ao serviço.

Assim toda atualização de uma API deve ser feita criando uma nova versão e mantendo uma versão anterior com um aviso aos clientes de que ela será desativada em um certo tempo e que deverão migrar para a nova versão.

Assim para criar uma versão da API, uma das abordagens é prefixar o endpoint com a versão da API :

https://api.exemplo.com/v1/produtos/2/

Aqui v1 e v2 indica a versão da API. Assim uma outra versão poderá ser identificada como:

https://api.exemplo.com/v2/produtos/2/

Dessa forma, sempre podemos incrementar nosso número de versão da API (por exemplo, v2, v3 ...) sempre que houver alterações significativas em nossa API. Isso também indica aos usuários que algo drástico mudou e eles devem ter cuidado ao usar a nova versão.

Abaixo veja um exemplo de implementação feito na Web API ASP .NET Core:

    [Route("api/[controller]")]
    [ApiController]

    public class ValuesController : ControllerBase
    {
        [HttpGet("v1")]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        [HttpGet("v2")]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "novo value1", "novo value2" };
        }
    ....
}

Aqui os métodos são iguais mas possuem um roteamento diferente que atua como um versionamento do método que possui duas versões v1 e v2 definidas em HttpGet.

O controle de versão da API não é uma tarefa simples, pois precisamos oferecer suporte à versão antiga e à nova versão e garantir que nossas alterações não interrompam a funcionalidade existente. Poderíamos ter novos clientes que querem usar as melhores e mais recentes funcionalidades enquanto outros querem apenas a versão antiga com as implementações básicas.
Implementações

Existem várias maneiras de usar o controle de versão da API e não existe uma resposta certa para todas. Cada caso é diferente com base em seus cenários de usuário e caso de usuário.

Precisamos sempre pensar em nossos clientes e como eles estão usando esta API e como podemos sempre alcançar os melhores resultados.

Podemos usar as seguintes as seguintes abordagens para versionar uma Web API:

  1. URI -  Define as versões da API na URI
    Ex: http://localhost:5000/api/2.0/teste/1
     

  2. Query String - Especifica a versão da API na string de consulta
    Ex: http://localhost:5000/api/teste/1?api-version=2.0 
     

  3. Header- Envia a versão no Header HTTP na chamada do controller
    Ex:  Definir uma chave :  x-api-version e o valor da versão e enviar no Header

Existem outras maneiras também, como o cabeçalho de aceitação e o tipo de conteúdo, mas neste artigo, vamos nos concentrar nos três métodos acima.

Versionamento na ASP .NET Core

Para fazer o controle de versão na API da Web do ASP.NET Core, podemos usar o pacote Microsoft.AspNetCore.Mvc.Versioning que fornecerá os métodos necessários para o controle de versão.

A seguir basta definir no método ConfigureServices a utilização do serviço:


services.AddApiVersioning();

 

E para mostrar isso na prática vamos criar um novo projeto usando o template ASP .NET Core Web API no Visual Studio 2019  com o nome APiDemoVersionamento usando as seguintes configurações:

Observe que não estamos habilitando o uso do Swagger em nosso projeto.

A seguir vamos deletar o arquivo WeatherForecast.cs e o controlador WeatherForecastController.

Vamos criar no projeto a pasta Models e nesta pasta vamos criar a classe User:

public class User
{
        public int Id { get; set; }
        public string Name { get; set; }
}

Na pasta Controllers vamos criar o controlador UsersController com o seguinte código:

using DemoAPIVersionamento.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace DemoAPIVersionamento.Controllers
{
    [ApiController]
    [Route("api/users")]
    public class UsersController : ControllerBase
    {
        [HttpGet("{userId}")]
        public IActionResult GetUser(int userId)

        {
            var user = new User
            {
                Id = userId,
                Name = "Macoratti"
            };

            return Ok(user);
        }

        [HttpGet()]
        public IActionResult AllUsers()

        {
            List<User> _users = new List<User>()
        {
            new User
            {
                Id = 1,
                Name = "Macoratti"
            },
            new User
            {
                Id = 2,
                Name = "Miriam"
            },
            new User
            {
                Id = 3,
                Name = "Janice"
            }
        };
        return Ok(_users);
        }
    }
}

Neste momento podemos executar o projeto e iremos obter o seguinte resultado:

Versionando a API

Vamos iniciar o processo de versionamento da nossa API.

Para isso vamos instalar o pacote Microsoft.AspNetCore.Mvc.Versioning no projeto via menu Tools-> ..->Manage Nuget Packages for Solution;

Na guia Browser selecione o pacote e clique no botão Install:

A seguir no arquivo Startup vamos definir a seguinte configuração no método ConfigureServices:

 public void ConfigureServices(IServiceCollection services)
 {

      services.AddControllers();           

      services.AddApiVersioning();  
 }

Agora vamos executar novamente o projeto e veremos que vamos obter o seguinte erro:

Este erro esta ocorrendo porque após adicionar a funcionalidade de controle de versão em nossa API nós ainda não configuramos a API e não informamos ao ASP.NET Core como ele vai lidar com o controle de versão da API.

Uma forma de contornar este erro e poder acessar a nossa API é incluir na string de consulta da URL a informação da versão inicial da API para isso basta acrescentar o parâmetro : api-version=1.0 na URL :   https://localhost:44388/api/users?api-version=1.0

Agora podemos acessar a nossa API mas isso não resolve o problema pois ainda estaremos 'quebrando' as implementações atuais com nossos clientes, precisamos de uma maneira melhor de abordar isso.  Vamos melhorar a nossa configuração do versionamento alterando o código definido no método ConfigureServices para:

 public void ConfigureServices(IServiceCollection services)
 {

      services.AddControllers();           

      services.AddApiVersioning(setup =>
      {
          setup.DefaultApiVersion = ApiVersion.Default;  //new ApiVersion(1, 0);
          setup.AssumeDefaultVersionWhenUnspecified = true;
          setup.ReportApiVersions = true;

    });
 }

Agora estamos estamos definindo :

Agora a API irá retornar a versão suportada nos cabeçalhos para que o cliente saiba todos os cabeçalhos disponíveis.

Aplicando o versionamento

Agora que já temos o versionamento da API configurado vamos aplicar o versionamento.

Para fazer isso vamos organizar os nosso controladores em pastas distintas criando os diretórios v1 e v2 na pasta Controllers e em cada pasta vamos incluir o controlador UsersController. Portanto, dentro da pasta de controladores, vamos adicionar 2 pastas v1, v2 e mover o UsersController para dentro de V1 e V2.

Assim na pasta v1 vamos incluir o controlador UsersController e vamos incluir o atributo [ApiVersion("1.0")] :

using DemoAPIVersionamento.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace DemoAPIVersionamento.Controllers.v1
{
    [ApiController]
    [Route("api/users")]

    [ApiVersion("1.0")]
    public class UsersController : ControllerBase
    {
        [HttpGet("{userId}")]
        public IActionResult GetUser(int userId)
        {
            var user = new User
            {
                Id = userId,
                Name = "Macoratti"
            };

            return Ok(user);
        }

        [HttpGet()]
        public IActionResult AllUsers()
        {
            List<User> _users = new List<User>()
        {
            new User
            {
                Id = 1,
                Name = "Macoratti"
            },
            new User
            {
                Id = 2,
                Name = "Miriam"
            },
            new User
            {
                Id = 3,
                Name = "Janice"
            }
        };
        return Ok(_users);
        }
    }
}


Uma versão da API contém uma versão principal e uma secundária, e a versão secundária às vezes é omitida como uma convenção e fica por conta dos desenvolvedores da API sobre como a versão deve ser especificada.
   
Uma versão da API contém uma versão principal e uma secundária, e a versão secundária às vezes é omitida como uma convenção e fica por conta dos desenvolvedores da API sobre como a versão deve ser especificada.

Agora vamos criar na pasta Models a classe UserV2 e definir o seguinte código:

 public class UserV2
 {
        public Guid Id { get; set; }
        public string Name { get; set; }
 }

Note que agora temos a classe UserV2 onde o Id é do tipo Guid e temos a classe User que o Id é do tipo int.

A classe User vai ser usada na versão 1.0 da API e a classe UserV2 vai ser usada na versão 2.0 da APi. Para isso vamos criar o controlador UsersController na pasta v2 com código abaixo:

using DemoAPIVersionamento.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace DemoAPIVersionamento.Controllers.v2
{
    [ApiController]
    [Route("api/users")]

    [ApiVersion("2.0")]
    public class UsersController : ControllerBase
    {

        [HttpGet("{userId}")]
        public IActionResult GetUser(Guid userId)
        {
             var user = new UserV2
             {
                   Id = userId,
                   Name = "Macoratti"
              };

              return Ok(user);
         }

        [HttpGet()]
        public IActionResult AllUsers()
        {
            List<UserV2> users = new List<UserV2>()
            {
                new UserV2
                {
                    Id = Guid.NewGuid(),
                    Name = "Macoratti"
                },
                new UserV2
                {
                    Id = Guid.NewGuid(),
                    Name = "Miriam"
                },
                new UserV2
                {
                    Id = Guid.NewGuid(),
                    Name = "Janice"
                }
            };

            return Ok(users);
        }
    }
}

Assim a versão v2 da nossa API esta usando o atributo [ApiVersion("2.0")] e retorna os usuários usando o novo modelo UserV2 onde o Id é do tipo Guid.

Agora ao executar o projeto novamente teremos o mesmo resultado inicial pois a versão padrão da API é a versão 1.0. Para poder acessar a versão 2.0 podemos incluir na url o parâmetro: ?api-version=2.0

Este seria o versionamento usando a query string que não é muito usado pois sempre é preciso informar o parâmetro na string de consulta da URL.

Vamos agora definir o versionamento via segmento de URL, e, para isso vamos alterar o atributo Route no controlador UsersControllers da versão 2.0 da API incluindo a versão da API usando:  {version:apiVersion} :

[ApiController]
[Route("api/{version:apiVersion}/users")]
[ApiVersion("2.0")]

Assim o controlador da versão v1 fica inalterado para continuar atendendo aos clientes da versão 1.0.

O atributo [ApiVersion] descreve para qual versão o controlador precisa ser mapeado, a versão é especificada na rota (neste caso), usando o fragmento "{version: apiVersion}" - que adiciona o número da versão definido no ApiVersion para a rota durante a correspondência. Nesse caso, "/api/v1.0/users"  mapeados para este controlador.

Nota: Poderíamos ter definido também o fragmento "v {version: apiVersion}" - e neste caso o número da versão estaria no ApiVersion para a rota durante a correspondência. Nesse caso, "/api/v1.0/users"  mapeado para este controlador.

Para acessar a versão 2.0 basta usar a seguinte url: https://localhost:44388/api/2.0/users

Para aplicar o versionamento via Header vamos incluir no arquivo Startup a configuração abaixo:

public void ConfigureServices(IServiceCollection services)
{
            services.AddControllers();
            services.AddApiVersioning(setup =>
            {
                setup.DefaultApiVersion = ApiVersion.Default; //new ApiVersion(1, 0);
                setup.AssumeDefaultVersionWhenUnspecified = true;
                setup.ReportApiVersions = true;

                setup.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
            });
 }

Agora podemos enviar no cabeçalho do request a informação da versão em : x-api-version e o valor 1.0 ou 2.0.

Para habilitar várias maneiras de aceitar versões de API, por exemplo, eu quero habilitar o cabeçalho de aceitação (Content-Type) e o cabeçalho personalizado ao mesmo tempo. Precisamos atualizar a classe Startup com o seguinte código:

public void ConfigureServices(IServiceCollection services)
 {
            services.AddControllers();
            services.AddApiVersioning(setup =>
            {
                setup.DefaultApiVersion = ApiVersion.Default; //new ApiVersion(1, 0);
                setup.AssumeDefaultVersionWhenUnspecified = true;
                setup.ReportApiVersions = true;

                setup.ApiVersionReader = ApiVersionReader.Combine(
                                                           new MediaTypeApiVersionReader("x-api-version"),
                                                           new HeaderApiVersionReader("x-api-version")
                );

            });           
}

Agora podemos enviar a informação da versão no Header ou no Content-Type.

Agora, digamos que queremos começar a descontinuar nossas APIs, a maneira de fazer isso é atualizar o atributo de versão da API usando a seguinte informação no atributo ApiVersion:  

 [ApiController]
 [Route("api/users")]
 [ApiVersion("1.0", Deprecated = true)]
public class UsersController : ControllerBase
{
...
}
Com isso, agora a API vai informar no Header ao cliente que esta versão está depreciada:

Pegue o projeto aqui:  DemoAPIVersionamento.zip

"Mas o fruto do Espírito é: amor, gozo, paz, longanimidade, benignidade, bondade, fé, mansidão, temperança.
Contra estas coisas não há lei."
Gálatas 5:22,23

Referências:


José Carlos Macoratti