DEV Community

Cover image for Criando métodos HTTP PATCH, usando o AutoMapper
Silvair L. Soares
Silvair L. Soares

Posted on • Edited on

Criando métodos HTTP PATCH, usando o AutoMapper

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 seguinte 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; }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
]'
Enter fullscreen mode Exit fullscreen mode

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;
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

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" },
     ...
]
Enter fullscreen mode Exit fullscreen mode

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".

Exemplo de requisição usando JSON Patch no Swagger

É 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}

    }    
}
Enter fullscreen mode Exit fullscreen mode

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 responsável por definir esta condição no AutoMapper:

.ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null));
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode
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
  }
}'
Enter fullscreen mode Exit fullscreen mode

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".

Exemplo de requisição usando JSON Patch no Swagger

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.

Veja aqui o código completo do projeto no GitHub.

Top comments (0)