ASP.NET Core -  APIs Idempotentes


  Neste artigo veremos o conceito de Idempotência e como criar APIs idempotentes.

O conceito de idempotência tem origem na matemática, e o termo idempotente é um jargão para dizer: “podemos aplicar esta operação várias vezes e ainda obter o mesmo resultado”.

Desta forma Idempotente é um termo que pode ser aplicado em diferentes contextos, mas geralmente significa que uma operação ou função pode ser aplicada várias vezes sem alterar o resultado final. Em outras palavras, se a operação ou função é aplicada uma ou várias vezes, o resultado final será o mesmo.

Um cenário bem comum é quendo temos um usuário que pode clicar duas vezes no botão enviar de um formulário, iniciando vários requests idênticos para uma aplicação Web, e , dependendo da aplicação isso pode ter efeitos colaterais inofensivos, como enviar uma comunicação duplicada ou pode causar uma situação estressante de debitar inúmeras vezes a sua conta bancária.

Assim, realizar operações idempotentes pode ser crucial, pois elas podem ser repetidas com segurança sem causar problemas. A idempotência nas Web APIs visa garantir que a API funcione corretamente mesmo quando os clientes enviarem a mesma requisição várias vezes. Por exemplo, este caso pode acontecer quando a API falhou ao gerar a resposta (devido a falhas de processo, indisponibilidade temporária, etc.) ou porque a resposta foi gerada, mas não pôde ser transferida devido a problemas de rede.

O protocolo HTTP possui métodos idempotentes. Na verdade, alguns dos métodos HTTP são projetados especificamente para serem idempotentes, o que significa que eles podem ser executados várias vezes sem alterar o estado do servidor ou do recurso. Os métodos HTTP idempotentes são:

  1. GET: o método GET é usado para recuperar informações de um servidor, e é idempotente porque não altera o estado do servidor ou do recurso.
  2. HEAD: o método HEAD é semelhante ao método GET, mas solicita apenas os cabeçalhos da resposta HTTP, sem o corpo da resposta. O método HEAD é idempotente porque também não altera o estado do servidor ou do recurso.
  3. PUT: o método PUT é usado para atualizar um recurso existente no servidor. Se o recurso já existe, o método PUT atualiza o estado do recurso com os dados fornecidos. O método PUT é idempotente porque, se a mesma solicitação PUT for enviada várias vezes, o estado do recurso ainda será o mesmo.
  4. DELETE: o método DELETE é usado para excluir um recurso do servidor. O método DELETE é idempotente porque, se o recurso já foi excluído, a solicitação DELETE subsequente não terá efeito e não alterará o estado do servidor ou do recurso.

A criação de um consumidor idempotente é um fator essencial na idempotência HTTP. O servidor da API precisaria de uma maneira de reconhecer tentativas subsequentes da mesma solicitação. Comumente, o consumidor gera um valor único, chamado idempotency-key, que o servidor da API utiliza para esse fim.

Para o exemplo que iremos usar neste artigo vamos usar o pacote Nuget IdempotenteAPI que implementa um atributo ASP.NET Core (filter) para lidar com as operações HTTP (POST e PATCH).

Esta biblioteca funciona da seguinte forma:

O consumidor da API (por exemplo, um site Front-End) envia uma solicitação incluindo um identificador exclusivo do cabeçalho Idempotency-Key (nome padrão: IdempotencyKey).

O servidor API verifica se esse identificador exclusivo foi usado anteriormente para essa solicitação e retorna a resposta em cache (sem execução adicional) ou salva em cache a resposta junto com o identificador exclusivo. A resposta em cache inclui o código de status HTTP, o corpo da resposta e os cabeçalhos.

O armazenamento de dados é necessário para a idempotência, mas se os dados não expirarem após um determinado período, isso incluirá complexidade desnecessária no armazenamento, segurança e dimensionamento de dados. Portanto, os dados devem ter um período de retenção que faça sentido para o domínio do seu problema.

A biblioteca IdempotentAPI realiza validação adicional da chave de hash da solicitação para garantir que a resposta em cache seja retornada para a mesma combinação de chave de idempotência e solicitação para evitar uso indevido acidental.

Para ilustrar , de forma básica o funcionamento deste biblioteca vamos criar um projeto ASP .NET Core Web API e mostrar a idempotência em ação.

recursos usados:

  • .NET 7.0
  • IdempotentAPI
  • IdempotentAPI.Cache.DistributedCache

Criando o projeto no VS 2022

Vamos criar uma Web API chamada APiIdempotente com as seguintes configurações :

A seguir vamos incluir no projeto os seguintes pacotes Nuget:

Estamos usando o segundo pacote pois vamos armazenar os dados na memória e não vamos usar um banco de dados.

A seguir precisamos registrar o serviço para API e para o cache em memória.

...

builder.Services.AddDistributedMemoryCache();

builder.Services.AddIdempotentAPIUsingDistributedCache();
...
 

A seguir como não vamos usar um banco de dados vamos criar os DTOS para o request e para o response considerando o registro de um Produto com Nome e Preco e DataCriacao.

Crie a pasta DTOs no projeto e nesta pasta crie a classe ProdutoDtoRequest e ProdutoDtoResponse:

1- ProdutoDtoRequest

public class ProdutoDtoRequest
{
 
public string? Nome { get; set; }
  public decimal Preco { get; set; }
}

2- ProdutoDtoRespose

public class ProdutoDtoResponse
{
   public int Id { get; set; }
  
public DateTime DataCriacao { get; set; }
}

Agora podemos criar o controlador.

Criando o controlador

Na pasta controllers vamos criar o controlador ProdutosController usando o código abaixo:

using ApiIdempotente.DTOs;
using
IdempotentAPI.Filters;
using
Microsoft.AspNetCore.Mvc;

namespace ApiIdempotente.Controllers;

[Route("api/[controller]")]
[ApiController]
[Produces(
"application/json")]
[Idempotent(Enabled =
true)]

public class ProdutosController : ControllerBase
{
   [HttpPost]
  
public IActionResult Create([FromBody] ProdutoDtoRequest produto)
   {
    
//Usar idempotent-key = 9fbeca16-e85f-4971-9263-54bf765ef729
    
var produtoResponse = new ProdutoDtoResponse()
     {
       Id = Guid.NewGuid(),
       DataCriacao = DateTime.Now
     };
    
return Ok(produtoResponse);
   }
}

O atributo [Idempotent] habilita a biblioteca IdempotentAPI, que é uma biblioteca para garantir que as solicitações POST sejam idempotentes. Isso significa que a solicitação pode ser executada várias vezes com o mesmo resultado sem criar dados duplicados ou alterar o estado do servidor.

Este endpoint pode ser usado para criar novos objetos Produto, com garantia de que a solicitação será idempotente, garantindo que a criação de um produto seja realizada apenas uma vez.

Incluindo o Header no request do Swagger

Para poder incluir no Header do request do Swagger, que para o nosso exemplo será o valor da chave Idempotency, vamos criar uma IOperationFilter customizada que vai incluir este header.

A interface IOperationFilter é uma interface que pode ser implementada em uma aplicação ASP.NET Core para modificar a descrição de uma operação de API (Endpoint) antes que ela seja exposta na interface do usuário da API (por exemplo, a página Swagger UI).

Quando o Swagger gera a documentação da API, ele usa uma variedade de fontes para criar a especificação, incluindo os atributos e as convenções do ASP.NET Core. No entanto, em alguns casos, essas fontes não fornecem informações suficientes para gerar uma especificação completa e precisa.

É aí que entra a interface IOperationFilter. Ela fornece um meio para adicionar, remover ou modificar a descrição de uma operação de API, permitindo que os desenvolvedores personalizem a especificação gerada pelo Swagger.

Por exemplo, você pode usar um IOperationFilter para definir descrições personalizadas, adicionar informações sobre autenticação e autorização, incluir exemplos de solicitação e resposta e muito mais. Essa interface é uma ferramenta poderosa para personalizar a documentação gerada pelo Swagger e tornar a API mais útil para seus usuários.

Crie uma pasta Utils no projeto e nesta pasta crie a classe AddHeaderParameter :

using Microsoft.OpenApi.Models;
using
Swashbuckle.AspNetCore.SwaggerGen;

namespace ApiIdempotente.Utils;

public class AddHeaderParameter : IOperationFilter
{
  
public void Apply(OpenApiOperation operation, OperationFilterContext context)
   {
     
if (operation.Parameters == null)
        operation.Parameters =
new List<OpenApiParameter>();

       operation.Parameters.Add(new OpenApiParameter
       {
          Name =
"IdempotencyKey",
          In = ParameterLocation.Header,
          Required =
true,
          Schema =
new OpenApiSchema
          {
             Type =
"string"
          }
       });
    }
}

A seguir vamos incluir na classe Program a configuração no serviço do Swagger para incluir este parâmetro no Header :

...

builder.Services.AddSwaggerGen(c =>
{
   c.SwaggerDoc(
"v1", new OpenApiInfo { Title = "IdempotentAPI", Version = "v1" });
       c.OperationFilter<AddHeaderParameter>();
});
...

Agora executando o projeto teremos o seguinte resultado:

Vamos realizar um post para criar um produto na memória usando o endpoint POST /api/Produtos e informando o valor de um GUID para informar o parâmetro no header do Request que representa o IdempotencyKey :

Clicando em Execute iremos obter o resultado abaixo:

Agora se você clicar mais de uma vez vai notar que a resposta não vai ser alterada pois estaremos usando os mesmos dados e o mesmo valor Guid para idempotencyKey.

Se você quiser testar no Postam basta copiar o endereço URI da API e definir o body do Request:

E definir também o valor do Header informando : idempotencyKey e o valor 9fbeca16-e85f-4971-9263-54bf765ef729 (pode ser qualquer guid)

O resultado será:

{
   "id":"8f67717c-ce60-441b-9d9d-0e97e5aaa16d",
   "dataCriacao":"2023-04-25T16:01:21.9475161-03:00"
}

Com isso temos uma forma simples e rápida de criar um endpoint idempotente na ASP.NET Core.

Pegue o projeto aqui : ApiIdempotente.zip ...

"Eu sou a videira, vós as varas; quem está em mim, e eu nele, esse dá muito fruto; porque sem mim nada podeis fazer."
João 15:5

Referências:


José Carlos Macoratti