ASP.NET Core - APIs Idempotentes
Neste artigo veremos o conceito de Idempotência e como criar APIs idempotentes. |
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:
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:
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.
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.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: