Protocolo HTTP
O protocolo HTTP (HyperText Transfer Protocol) é a base da comunicação na web, definindo como os clientes (navegadores, aplicativos) se comunicam com os servidores para solicitar e receber dados. Uma parte fundamental desse protocolo são os métodos HTTP, que indicam a ação que se deseja realizar sobre um recurso.
Métodos HTTP
Métodos HTTP, são um conjunto de convenções definidas pelo protocolo HTTP, através dos quais, usuários de um serviço web podem solicitar à um servidor web, que uma ação específica seja executada.
Os métodos HTTP mais comuns
GET: Utilizado para obter ou recuperar dados de um recurso específico. É o método mais simples e idempotente (pode ser repetido várias vezes sem alterar o resultado).
POST: Empregado para criar novos recursos. É frequentemente utilizado para enviar dados para um servidor, como em formulários.
PUT: Serve para atualizar um recurso completamente. Substitui a representação atual do recurso por uma nova.
DELETE: Usado para excluir um recurso.
PATCH: Similar ao PUT, mas permite atualizar apenas partes específicas de um recurso.
HEAD: Retorna apenas os cabeçalhos da resposta, sem o corpo. É útil para verificar o status de um recurso sem baixar todo o conteúdo.
Neste artigo, apresentarei uma proposta de implementação de um endpoint HTTP PATCH.
Este tipo de serviço, apresenta algumas nuances um pouco diferentes e até desafiadoras, em comparação aos demais métodos HTTP.
Serviços que atendem requisições do tipo PATCH, geralmente exigem que apenas a parte do recurso a ser modificado seja enviada, o que pode tornar a operação do lado do cliente um pouco mais complexa, pois o payload a ser enviado se torna dinâmico.
Para lidar com esta complexidade, foi criado RFC 6902 chamado JSON Patch, o qual define um padrão para transmissão das informações que devem ser modificadas do lado do servidor.
O JSON Patch oferece ao usuário ações como inclusão, exclusão, substituição, etc de apenas uma parte do recurso armazenado no servidor.
Veja neste artigo, como configurar a implementação oficial para .Net.
Ao longo deste artigo, vamos construir uma API para gestão de Pessoas.
Tendo como exemplo, o seguinde conjunto de classes em C#:
public class Endereco
{ public string? Rua { get; set; } public string? Complemento { get; set; }
} public class Pessoa
{ public string? Nome { get; set; } public int? Idade { get; set; } public Endereco? Endereco { get; set; }
}
Uma requisição utilizando JSON Patch, que tenha interesse em modificar uma parte do recurso que tenha sido salvo com o tipo Pessoa, definido anteriormente, seria algo como:
[ { "path": "/nome", "op": "replace", "value": "Silvair Leite Soares - Nome alterado via jsonpatch" }, { "path": "/endereco/rua", "op": "replace", "value": "Rua do endereço alterada" }
]
A implementação do serviço que recebe este payload, seria algo como:
Endpoint que executa a atualização via JsonPath:
using HttpPatchWithAutoMapper.Domain.Pessoas;
using HttpPatchWithAutoMapper.Domain.Pessoas.ViewModels;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc; namespace HttpPatchWithAutoMapper.Controllers
{ [ApiController] [Route("[controller]")] public class PessoasController : ControllerBase { private readonly IPessoasServices _pessoasServices; public PessoasController(IPessoasServices pessoasServices) { _pessoasServices = pessoasServices; } /// <summary> /// Atualiza parcialmente uma pessoa usando JSON Patch /// </summary> /// <returns></returns> [HttpPatch("api/pessoas/JsonPatch/")] public async Task<Pessoa> UpdateJsonPatch(string id, [FromBody] JsonPatchDocument<Pessoa> pessoa) { return await _pessoasServices.UpdateJsonPatch(id, pessoa); } }
}
Demonstração de uma requisição:
curl -X 'PATCH' \ 'https://localhost:7281/Pessoas/api/pessoas/JsonPatch?id=d892792b-a7ac-481e-b0af-5adc62d51ce4' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '[ { "path": "/nome", "op": "replace", "value": "Silvair Leite Soares - Nome alterado via jsonpatch" }, { "path": "/endereco/rua", "op": "replace", "value": "Rua do endereço alterado" }
]'
Implementação do serviço que executa a atualização via JsonPath:
using AutoMapper;
using HttpPatchWithAutoMapper.Domain.Pessoas.ViewModels;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.Mvc; namespace HttpPatchWithAutoMapper.Domain.Pessoas
{ public class PessoasServices : IPessoasServices { private static ICollection<Pessoa> pessoas = new List<Pessoa>(); public async Task<Pessoa> UpdateJsonPatch(string id, [FromBody] JsonPatchDocument<Pessoa> pessoa) { var pessoaSalva = pessoas.FirstOrDefault(p => p.Id == id); if (pessoaSalva == null) { throw new ArgumentException("Pessoa não encontrada"); } try { // Aqui é o local em que os comandos definidos no objeto 'pessoa' // são aplicados, atualizando o objeto 'pessoaSalva' pessoa.ApplyTo(pessoaSalva); return pessoaSalva; } catch (JsonPatchException ex) { throw ex; } catch (Exception ex) { throw ex; } }
}
Esta implementação funciona bem, porém o processo de geração do payload de atualização do lado do cliente, se torna extremamente complexo. Pois precisamos criar uma operação para cada propriedade que o usuário deseja alterar. Dessa forma, o payload para atualização dos recursos do servidor, tende a ficar cada vez maior e mais complexo
[ { "op": "replace", "path": "/a/b/c", "value": "novo valor" }, { "op": "replace", "path": "/a/b/c", "value": "C" }, ...
]
Na imagem abaixo, um exemplo de requisição no Swagger, que altera o nome e a rua do endereço da pessoa com o id “d892792b-a7ac-481e-b0af-5adc62d51ce4”.
É um payload bem verboso para executar pequenas alterações. Além de obrigamos que o cliente de nossa API entenda o padrão definido na RFC 6902.
Neste ponto o AutoMapper pode contribuir bastante. Utilizando-o, podemos criar um endpoint que recebe o mesmo payload usado para fazer uma inclusão (HTTP POST) ou atualização completa (HTTP PUT):
{ "id": "d892792b-a7ac-481e-b0af-5adc62d51ce4", "nome": "Silvair Leite Soares", "idade": "40", "endereco": { "rua": "Nome da rua da pessoa", "complemento": "Complemento do endereço" }
}
De forma que as propriedades que forem omitidas (com valor igual à null), permanecerão inalteradas no repositório de dados.
{ "id": "d892792b-a7ac-481e-b0af-5adc62d51ce4", "nome": "Silvair Leite Soares - Nome alterado via jsonpatch", "idade": null, "endereco": { "rua": "Nome da rua da pessoa", "complemento": null }
}
Vamos à implementação.
O primeiro passo é incluir a referência ao pacote nuget do AutoMapper no projeto. Para isso execute o comando:
dotnet add package AutoMapper --version 13.0.1
Endpoint que executa a atualização via AutoMapper:
using HttpPatchWithAutoMapper.Domain.Pessoas;
using HttpPatchWithAutoMapper.Domain.Pessoas.ViewModels;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc; namespace HttpPatchWithAutoMapper.Controllers
{ [ApiController] [Route("[controller]")] public class PessoasController : ControllerBase { private readonly IPessoasServices _pessoasServices; public PessoasController(IPessoasServices pessoasServices) { _pessoasServices = pessoasServices; } /// <summary> /// Atualiza parcialmente uma pessoa usando o AutoMapper /// </summary> /// <returns></returns> [HttpPatch("api/pessoas/AutoMapper")] public async Task<Pessoa> UpdateAutoMapper([FromBody] Pessoa pessoa) { return await _pessoasServices.UpdateAutoMapper(pessoa); } }
}
Implementação do serviço que executa a atualização via AutoMapper:
using AutoMapper;
using HttpPatchWithAutoMapper.Domain.Pessoas.ViewModels;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.Mvc; namespace HttpPatchWithAutoMapper.Domain.Pessoas
{ public class PessoasServices : IPessoasServices { private static ICollection<Pessoa> pessoas = new List<Pessoa>(); public Task<Pessoa> UpdateAutoMapper(Pessoa pessoa)
{ var existingPessoa = pessoas.FirstOrDefault(p => p.Id == pessoa.Id); if (existingPessoa == null) { throw new ArgumentException("Pessoa não encontrada"); } // Configuração do AutoMapper var configAutoMapper = new MapperConfiguration(cfg => { cfg.CreateMap<Pessoa, Pessoa>() .ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); cfg.CreateMap<Endereco, Endereco>() .ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); }); // Inclui configuração de mapeamento personalizada, para copiar apenas itens não nulos var mapper = configAutoMapper.CreateMapper(); // Faz o merge das informações salvas no repositório, // com as propriedades não nulas enviadas pelo usuário da API mapper.Map(pessoa, existingPessoa); return Task.FromResult(existingPessoa);
} } }
Precisamos configurar cada uma das propriedades de tipos complexos (Pessoa e Endereco), que deveremos copiar apenas as propriedades não nulas, ou seja, as propriedades que o usuário da API enviou na requisição.
Esta instrução é a reponsável por definir esta condição no AutoMapper:
.ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null));
Para atualizar apenas algumas propriedades do recurso que foi anteriormente incluido no repositório de dados, poderemos enviar requisições simples assim:
{ "id": "d892792b-a7ac-481e-b0af-5adc62d51ce4", "nome": "Silvair Leite Soares - Nome alterado via AutoMapper", "idade": null, // <-Esta propriedade permanecerá inalterada "endereco": { "rua": "Nome da rua da pessoa alterada via AutoMapper", "complemento": null // <-Esta propriedade permanecerá inalterada }
}
curl -X 'PATCH' \ 'https://localhost:7281/Pessoas/api/pessoas/AutoMapper' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "id": "d892792b-a7ac-481e-b0af-5adc62d51ce4", "nome": "Silvair Leite Soares - Nome alterado via AutoMapper", "idade": null, "endereco": { "rua": "Nome da rua da pessoa alterada via AutoMapper", "complemento": null }
}'
Na imagem abaixo, um exemplo de requisição no Swagger, que altera o nome e a rua do endereço da pessoa com o id “d892792b-a7ac-481e-b0af-5adc62d51ce4”.
Esta proposta exige um pouco mais de configuração, mas em contrapartida simplifica absurdamente a operação por parte do cliente da API. Já que o payload exigido pelos métodos POST, PUT e PATCH, serão os mesmos.