Atualização parcial com JSON Patch
Resumo
PATCHatualiza apenas parte de um recurso. Com o formato JSON Patch (RFC 6902), o cliente envia uma lista de operações (replace,add,remove) aplicadas sobre um DTO, com validação após o patch.
1. Objetivos de Aprendizagem#
- Diferenciar
PATCHdePUT. - Implementar
PATCHcomJsonPatchDocument<T>. - Aplicar o patch a um DTO e revalidar o
ModelState.
2. Pré-requisitos#
- Atualização com
PUT(Módulo 25).
3. Conceito#
PATCH aplica uma modificação parcial. O formato JSON Patch descreve operações:
[
{ "op": "replace", "path": "/preco", "value": 129.9 },
{ "op": "replace", "path": "/nome", "value": "Mouse Ultra" }
]
O porquê de aplicar ao DTO (e não à entidade): controlar quais campos são alteráveis e revalidar antes de persistir.
4. Mão na Massa#
4.1. Setup#
dotnet add Catalog.Api package Microsoft.AspNetCore.Mvc.NewtonsoftJson
dotnet add Catalog.Api package Microsoft.AspNetCore.JsonPatch
Habilite o Newtonsoft (necessário para JSON Patch):
builder.Services.AddControllers()
.AddNewtonsoftJson();
Notao input formatter de JSON Patch depende do Newtonsoft.Json. Ative-o apenas para suportar
application/json-patch+json; o restante da API pode continuar comSystem.Text.Jsonconforme sua configuração.
4.2. Implementação Passo a Passo#
- Serviço: retorna o par (DTO para patch, entidade rastreada):
public async Task<(ProdutoForUpdateDto dto, Produto entity)> GetForPatchAsync(Guid categoriaId, Guid id)
{
var produto = await _repo.Produto.GetByIdAsync(categoriaId, id, trackChanges: true)
?? throw new ProdutoNotFoundException(id);
var dto = _mapper.Map<ProdutoForUpdateDto>(produto);
return (dto, produto);
}
public async Task SavePatchAsync(ProdutoForUpdateDto dto, Produto entity)
{
_mapper.Map(dto, entity);
await _repo.SaveAsync();
}
- Controller:
[HttpPatch("{id:guid}")]
public async Task<IActionResult> Patch(Guid categoriaId, Guid id,
[FromBody] JsonPatchDocument<ProdutoForUpdateDto> patchDoc)
{
if (patchDoc is null) return BadRequest("patchDoc não pode ser nulo.");
var (dto, entity) = await _service.Produto.GetForPatchAsync(categoriaId, id);
patchDoc.ApplyTo(dto, ModelState);
TryValidateModel(dto); // revalida após o patch
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
await _service.Produto.SavePatchAsync(dto, entity);
return NoContent();
}
4.3. Executando#
curl -i -X PATCH http://localhost:5000/api/categorias/{catId}/produtos/{id} \
-H "Content-Type: application/json-patch+json" \
-d '[{"op":"replace","path":"/preco","value":129.9}]'
5. Exemplo Completo#
O fluxo: buscar entidade rastreada → mapear para DTO → ApplyTo no DTO com ModelState → TryValidateModel → mapear DTO de volta para a entidade → salvar.
6. Boas Práticas e Armadilhas#
| Faça | Evite |
|---|---|
Passar o ModelState ao ApplyTo | Ignorar erros de patch (paths inválidos) |
Revalidar com TryValidateModel após aplicar | Persistir um DTO possivelmente inválido |
| Aplicar o patch ao DTO, não à entidade | Deixar o cliente alterar campos internos via path |
Atençãoaplicar JSON Patch diretamente na entidade permite ao cliente alterar campos não previstos (
/categoriaId,/id). Sempre patch no DTO restrito.
7. Segurança e Produção#
- JSON Patch é poderoso; limite os
pathspermitidos usando um DTO enxuto. - Habilitar Newtonsoft globalmente muda a serialização — teste os endpoints existentes após ativar.
8. Exercícios#
- Fácil: faça um patch que altere só o nome.
- Médio: retorne
422quando o patch tornar o preço negativo (validação). - Desafio: suporte JSON Patch e JSON Merge Patch (RFC 7386) no mesmo recurso.
9. Resumo#
PATCH faz atualização parcial via JSON Patch aplicado a um DTO restrito, com revalidação de ModelState. Isso mantém segurança e integridade, ao contrário de aplicar o patch direto na entidade.
10. Próximos Passos#
Módulo 27: validação — ModelState, atributos e validação por verbo.
11. Referências#
- Microsoft Learn — JsonPatch no ASP.NET Core web API
- RFC 6902 — JavaScript Object Notation (JSON) Patch
- Microsoft Learn — Formato de saída Newtonsoft.Json