JsonPatch na API Web do ASP.NET Core

Este artigo explica como lidar com solicitações JSON Patch em uma API Web do ASP.NET Core.

Instalação do pacote

JSO suporte ao ON Patch na API Web do ASP.NET Core é baseado em Newtonsoft.Json e exige o pacote NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson. Para habilitar o suporte ao JSON Patch:

  • Instale o pacote do NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Chame AddNewtonsoftJson. Por exemplo:

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers()
        .AddNewtonsoftJson();
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

AddNewtonsoftJson substitui os formatadores padrão de entrada e saída baseados em System.Text.Json usados para formatar todoJSo conteúdo ON. Esse método de extensão é compatível com os seguintes métodos de registro do serviço MVC:

O JsonPatch exige a configuração do cabeçalho Content-Type como application/json-patch+json.

Adicionar suporte ao JSON Patch ao usar System.Text.Json

O formatador de entrada baseado em System.Text.Json não suporta JSON Patch. Para adicionar suporte ao JSON Patch usando Newtonsoft.Json, deixando inalterados os outros formatadores de entrada e saída:

  • Instale o pacote do NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Atualizar Program.cs:

    using JsonPatchSample;
    using Microsoft.AspNetCore.Mvc.Formatters;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter());
    });
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Formatters;
    using Microsoft.Extensions.Options;
    
    namespace JsonPatchSample;
    
    public static class MyJPIF
    {
        public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
        {
            var builder = new ServiceCollection()
                .AddLogging()
                .AddMvc()
                .AddNewtonsoftJson()
                .Services.BuildServiceProvider();
    
            return builder
                .GetRequiredService<IOptions<MvcOptions>>()
                .Value
                .InputFormatters
                .OfType<NewtonsoftJsonPatchInputFormatter>()
                .First();
        }
    }
    

O código anterior cria uma instância de NewtonsoftJsonPatchInputFormatter e a insere como a primeira entrada na coleção MvcOptions.InputFormatters. Essa ordem de registro assegura que:

  • NewtonsoftJsonPatchInputFormatter processa JSsolicitações ON Patch.
  • A entrada e os formatadores baseados em System.Text.Json existentes processam todas as outras solicitações e respostas JSON.

Use o método Newtonsoft.Json.JsonConvert.SerializeObject para serializar um JsonPatchDocument.

Método de solicitação HTTP PATCH

Os métodos PUT e PATCH são usados para atualizar um recurso existente. A diferença entre eles é que PUT substitui o recurso inteiro, enquanto PATCH especifica apenas as alterações.

JSON Patch

JSON Patch é um formato para especificar as atualizações a serem aplicadas a um recurso. Um documento JSON Patch tem uma matriz de operações. Cada operação identifica um tipo específico de alteração. Exemplos dessas alterações incluem a adição de um elemento de matriz ou a substituição de um valor de propriedade.

Por exemplo, os seguintes JSdocumentos ON representam um recurso, um JSdocumento ON Patch para o recurso e o resultado da aplicação das operações de Patch.

Exemplo de recurso

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSExemplo de ON Patch

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

No JSON anterior:

  • A propriedade op indica o tipo de operação.
  • A propriedade path indica o elemento a ser atualizado.
  • A propriedade value fornece o novo valor.

Recurso depois do patch

Aqui está o recurso após a aplicação do JSdocumento ON Patch anterior:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

As alterações feitas pela aplicação de um JSdocumento ON Patch a um recurso são atômicas. Se alguma operação da lista falhar, nenhuma operação da lista será aplicada.

Sintaxe de path

A propriedade path de um objeto de operação tem barras entre os níveis. Por exemplo, "/address/zipCode".

Índices baseados em zero são usados para especificar os elementos da matriz. O primeiro elemento da matriz addresses estaria em /addresses/0. Para add até o final de uma matriz, use um hífen (-) em vez de um número de índice: /addresses/-.

Operações

A tabela a seguir mostra as operações suportadas, conforme definido na especificação JSON Patch:

Operação Observações
add Adicione uma propriedade ou elemento de matriz. Para a propriedade existente: defina o valor.
remove Remova uma propriedade ou elemento de matriz.
replace É o mesmo que remove, seguido por add no mesmo local.
move É o mesmo que remove da origem, seguido por add ao destino usando um valor da origem.
copy É o mesmo que add ao destino usando um valor da origem.
test Retorna o código de status de êxito se o valor em path é igual ao value fornecido.

JSON Patch no ASP.NET Core

A implementação do ASP.NET Core do JSON Patch é fornecida no pacote NuGet Microsoft.AspNetCore.JsonPatch.

Código do método de ação

Em um controlador de API, um método de ação para JSON Patch:

Veja um exemplo:

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

Esse código do aplicativo de exemplo funciona com o seguinte modelo Customer:

namespace JsonPatchSample.Models;

public class Customer
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
}
namespace JsonPatchSample.Models;

public class Order
{
    public string OrderName { get; set; }
    public string OrderType { get; set; }
}

O exemplo de método de ação:

  • Constrói um Customer.
  • Aplica o patch.
  • Retorna o resultado no corpo da resposta.

Em um aplicativo real, o código recuperaria os dados de um repositório, como um banco de dados, e atualizaria o banco de dados após a aplicação do patch.

Estado do modelo

O exemplo de método de ação anterior chama uma sobrecarga de ApplyTo que utiliza o estado do modelo como um de seus parâmetros. Com essa opção, você pode receber mensagens de erro nas respostas. O exemplo a seguir mostra o corpo de uma resposta 400 Solicitação Incorreta para uma operação test:

{
  "Customer": [
    "The current value 'John' at path 'customerName' != test value 'Nancy'."
  ]
}

Objetos dinâmicos

O exemplo do método de ação a seguir mostra como aplicar um patch a um objeto dinâmico:

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

A operação add

  • Se path aponta para um elemento de matriz: insere um novo elemento antes do especificado por path.
  • Se path aponta para uma propriedade: define o valor da propriedade.
  • Se path aponta para um local não existente:
    • Se o recurso no qual fazer patch é um objeto dinâmico: adiciona uma propriedade.
    • Se o recurso no qual fazer patch é um objeto estático: a solicitação falha.

O exemplo de documento de patch a seguir define o valor de CustomerName e adiciona um objeto Order ao final da matriz Orders.

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

A operação remove

  • Se path aponta para um elemento de matriz: remove o elemento.
  • Se path aponta para uma propriedade:
    • Se o recurso no qual fazer patch é um objeto dinâmico: remove a propriedade.
    • Se o recurso no qual fazer patch é um objeto estático:
      • Se a propriedade é anulável: define como nulo.
      • Se a propriedade não é anulável: define como default<T>.

O seguinte exemplo de documento de patch define CustomerName como nulo e exclui Orders[0]:

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

A operação replace

Esta operação é funcionalmente a mesma que remove seguida por add.

O exemplo de documento de patch a seguir define o valor de CustomerName e substitui Orders[0] por um novo objeto Order:

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

A operação move

  • Se path aponta para um elemento de matriz: copia o elemento from para o local do elemento path e, em seguida, executa uma operação remove no elemento from.
  • Se path aponta para uma propriedade: copia o valor da propriedade from para a propriedade path, depois executa uma operação remove na propriedade from.
  • Se path aponta para uma propriedade não existente:
    • Se o recurso no qual fazer patch é um objeto estático: a solicitação falha.
    • Se o recurso no qual fazer patch é um objeto dinâmico: copia a propriedade from para o local indicado por path e, em seguida, executa uma operação remove na propriedade from.

O seguinte exemplo de documento de patch:

  • Copia o valor de Orders[0].OrderName para CustomerName.
  • Define Orders[0].OrderName como nulo.
  • Move Orders[1] para antes de Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

A operação copy

Esta operação é funcionalmente a mesma que uma operação move, sem a etapa final remove.

O seguinte exemplo de documento de patch:

  • Copia o valor de Orders[0].OrderName para CustomerName.
  • Insere uma cópia de Orders[1] antes de Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

A operação test

Se o valor no local indicado por path for diferente do valor fornecido em value, a solicitação falhará. Nesse caso, toda a solicitação de PATCH falhará, mesmo se todas as outras operações no documento de patch forem bem-sucedidas.

A operação test normalmente é usada para impedir uma atualização quando há um conflito de simultaneidade.

O seguinte exemplo de documento de patch não terá nenhum efeito se o valor inicial de CustomerName for "John", porque o teste falha:

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

Obter o código

Exibir ou baixar o código de exemplo. (Como baixar.)

Para testar o exemplo, execute o aplicativo e envie solicitações HTTP com as seguintes configurações:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Método HTTP: PATCH
  • Cabeçalho: Content-Type: application/json-patch+json
  • Corpo: copie e cole um dos exemplos de JSdocumento ON Patch da pasta do projeto JSON.

Recursos adicionais

Este artigo explica como lidar com solicitações JSON Patch em uma API Web do ASP.NET Core.

Instalação do pacote

Para habilitar o suporte ao JSON Patch no seu aplicativo, conclua as etapas a seguir:

  1. Instale o pacote do NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  2. Atualize o método Startup.ConfigureServices do projeto para chamar AddNewtonsoftJson. Por exemplo:

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson é compatível com os métodos de registro do serviço MVC:

JSON Patch, AddNewtonsoftJson e System.Text.Json

AddNewtonsoftJson substitui os formatadores de entrada e saída baseados em System.Text.Json usados para formatar todoJSo o conteúdo ON. Para adicionar suporte ao JSON Patch usando Newtonsoft.Json, deixando os outros formatadores inalterados, atualize o método Startup.ConfigureServices do projeto da seguinte forma:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
    });
}

private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
    var builder = new ServiceCollection()
        .AddLogging()
        .AddMvc()
        .AddNewtonsoftJson()
        .Services.BuildServiceProvider();

    return builder
        .GetRequiredService<IOptions<MvcOptions>>()
        .Value
        .InputFormatters
        .OfType<NewtonsoftJsonPatchInputFormatter>()
        .First();
}

O código anterior exige o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson e as seguintes instruções using:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;

Use o método Newtonsoft.Json.JsonConvert.SerializeObject para serializar um JsonPatchDocument.

Método de solicitação HTTP PATCH

Os métodos PUT e PATCH são usados para atualizar um recurso existente. A diferença entre eles é que PUT substitui o recurso inteiro, enquanto PATCH especifica apenas as alterações.

JSON Patch

JSON Patch é um formato para especificar as atualizações a serem aplicadas a um recurso. Um documento JSON Patch tem uma matriz de operações. Cada operação identifica um tipo específico de alteração. Exemplos dessas alterações incluem a adição de um elemento de matriz ou a substituição de um valor de propriedade.

Por exemplo, os seguintes JSdocumentos ON representam um recurso, um JSdocumento ON Patch para o recurso e o resultado da aplicação das operações de Patch.

Exemplo de recurso

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSExemplo de ON Patch

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

No JSON anterior:

  • A propriedade op indica o tipo de operação.
  • A propriedade path indica o elemento a ser atualizado.
  • A propriedade value fornece o novo valor.

Recurso depois do patch

Aqui está o recurso após a aplicação do JSdocumento ON Patch anterior:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

As alterações feitas pela aplicação de um JSdocumento ON Patch a um recurso são atômicas. Se alguma operação da lista falhar, nenhuma operação da lista será aplicada.

Sintaxe de path

A propriedade path de um objeto de operação tem barras entre os níveis. Por exemplo, "/address/zipCode".

Índices baseados em zero são usados para especificar os elementos da matriz. O primeiro elemento da matriz addresses estaria em /addresses/0. Para add até o final de uma matriz, use um hífen (-) em vez de um número de índice: /addresses/-.

Operações

A tabela a seguir mostra as operações suportadas, conforme definido na especificação JSON Patch:

Operação Observações
add Adicione uma propriedade ou elemento de matriz. Para a propriedade existente: defina o valor.
remove Remova uma propriedade ou elemento de matriz.
replace É o mesmo que remove, seguido por add no mesmo local.
move É o mesmo que remove da origem, seguido por add ao destino usando um valor da origem.
copy É o mesmo que add ao destino usando um valor da origem.
test Retorna o código de status de êxito se o valor em path é igual ao value fornecido.

JSON Patch no ASP.NET Core

A implementação do ASP.NET Core do JSON Patch é fornecida no pacote NuGet Microsoft.AspNetCore.JsonPatch.

Código do método de ação

Em um controlador de API, um método de ação para JSON Patch:

  • É anotado com o atributo HttpPatch.
  • Aceita um JsonPatchDocument<T>, normalmente com [FromBody].
  • Chama ApplyTo no documento de patch para aplicar as alterações.

Veja um exemplo:

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

Esse código do aplicativo de exemplo funciona com o seguinte modelo Customer:

using System.Collections.Generic;

namespace JsonPatchSample.Models
{
    public class Customer
    {
        public string CustomerName { get; set; }
        public List<Order> Orders { get; set; }
    }
}
namespace JsonPatchSample.Models
{
    public class Order
    {
        public string OrderName { get; set; }
        public string OrderType { get; set; }
    }
}

O exemplo de método de ação:

  • Constrói um Customer.
  • Aplica o patch.
  • Retorna o resultado no corpo da resposta.

Em um aplicativo real, o código recuperaria os dados de um repositório, como um banco de dados, e atualizaria o banco de dados após a aplicação do patch.

Estado do modelo

O exemplo de método de ação anterior chama uma sobrecarga de ApplyTo que utiliza o estado do modelo como um de seus parâmetros. Com essa opção, você pode receber mensagens de erro nas respostas. O exemplo a seguir mostra o corpo de uma resposta 400 Solicitação Incorreta para uma operação test:

{
    "Customer": [
        "The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
    ]
}

Objetos dinâmicos

O exemplo do método de ação a seguir mostra como aplicar um patch a um objeto dinâmico:

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

A operação add

  • Se path aponta para um elemento de matriz: insere um novo elemento antes do especificado por path.
  • Se path aponta para uma propriedade: define o valor da propriedade.
  • Se path aponta para um local não existente:
    • Se o recurso no qual fazer patch é um objeto dinâmico: adiciona uma propriedade.
    • Se o recurso no qual fazer patch é um objeto estático: a solicitação falha.

O exemplo de documento de patch a seguir define o valor de CustomerName e adiciona um objeto Order ao final da matriz Orders.

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

A operação remove

  • Se path aponta para um elemento de matriz: remove o elemento.
  • Se path aponta para uma propriedade:
    • Se o recurso no qual fazer patch é um objeto dinâmico: remove a propriedade.
    • Se o recurso no qual fazer patch é um objeto estático:
      • Se a propriedade é anulável: define como nulo.
      • Se a propriedade não é anulável: define como default<T>.

O seguinte exemplo de documento de patch define CustomerName como nulo e exclui Orders[0]:

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

A operação replace

Esta operação é funcionalmente a mesma que remove seguida por add.

O exemplo de documento de patch a seguir define o valor de CustomerName e substitui Orders[0] por um novo objeto Order:

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

A operação move

  • Se path aponta para um elemento de matriz: copia o elemento from para o local do elemento path e, em seguida, executa uma operação remove no elemento from.
  • Se path aponta para uma propriedade: copia o valor da propriedade from para a propriedade path, depois executa uma operação remove na propriedade from.
  • Se path aponta para uma propriedade não existente:
    • Se o recurso no qual fazer patch é um objeto estático: a solicitação falha.
    • Se o recurso no qual fazer patch é um objeto dinâmico: copia a propriedade from para o local indicado por path e, em seguida, executa uma operação remove na propriedade from.

O seguinte exemplo de documento de patch:

  • Copia o valor de Orders[0].OrderName para CustomerName.
  • Define Orders[0].OrderName como nulo.
  • Move Orders[1] para antes de Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

A operação copy

Esta operação é funcionalmente a mesma que uma operação move, sem a etapa final remove.

O seguinte exemplo de documento de patch:

  • Copia o valor de Orders[0].OrderName para CustomerName.
  • Insere uma cópia de Orders[1] antes de Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

A operação test

Se o valor no local indicado por path for diferente do valor fornecido em value, a solicitação falhará. Nesse caso, toda a solicitação de PATCH falhará, mesmo se todas as outras operações no documento de patch forem bem-sucedidas.

A operação test normalmente é usada para impedir uma atualização quando há um conflito de simultaneidade.

O seguinte exemplo de documento de patch não terá nenhum efeito se o valor inicial de CustomerName for "John", porque o teste falha:

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

Obter o código

Exibir ou baixar o código de exemplo. (Como baixar.)

Para testar o exemplo, execute o aplicativo e envie solicitações HTTP com as seguintes configurações:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Método HTTP: PATCH
  • Cabeçalho: Content-Type: application/json-patch+json
  • Corpo: copie e cole um dos exemplos de JSdocumento ON Patch da pasta do projeto JSON.

Recursos adicionais