Compartilhar via


Suporte a patch JSON na API Web do ASP.NET Core

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

O suporte a JSON Patch na API Web do ASP.NET Core é baseado na serialização System.Text.Json e requer o pacote NuGet Microsoft.AspNetCore.JsonPatch.SystemTextJson.

O que é o padrão de patch JSON?

O padrão de patch JSON:

  • É um formato padrão para descrever as alterações a serem aplicadas a um documento JSON.

  • É definido no RFC 6902 e é amplamente usado em APIs RESTful para executar atualizações parciais para recursos JSON.

  • Descreve uma sequência de operações que modificam um documento JSON, como:

    • add
    • remove
    • replace
    • move
    • copy
    • test

Em aplicativos Web, o Patch JSON geralmente é usado em uma operação PATCH para executar atualizações parciais de um recurso. Em vez de enviar todo o recurso para uma atualização, os clientes podem enviar um documento de patch JSON contendo apenas as alterações. A aplicação de patch reduz o tamanho da carga e melhora a eficiência.

Para obter uma visão geral do padrão de Patch JSON, consulte jsonpatch.com.

Suporte a patch JSON na API Web do ASP.NET Core

O suporte ao JSON Patch na API Web do ASP.NET Core baseia-se na serialização System.Text.Json, começando com o .NET 10, implementando Microsoft.AspNetCore.JsonPatch com base na serialização System.Text.Json. Este recurso:

Observação

A implementação de Microsoft.AspNetCore.JsonPatch baseada na serialização System.Text.Json não é uma substituição direta para a implementação antiga baseada em Newtonsoft.Json. Ele não dá suporte a tipos dinâmicos, por exemplo ExpandoObject.

Importante

O padrão de patch JSON tem riscos de segurança inerentes. Como esses riscos são inerentes ao padrão de Patch JSON, a implementação do ASP.NET Core não tenta reduzir os riscos inerentes à segurança. É responsabilidade do desenvolvedor garantir que o documento do Patch JSON seja seguro para aplicar ao objeto de destino. Para obter mais informações, consulte a seção Mitigando riscos de segurança .

Habilitar o suporte a patch JSON com System.Text.Json

Para habilitar o suporte a JSON Patch com System.Text.Json, instale o pacote NuGet Microsoft.AspNetCore.JsonPatch.SystemTextJson.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease

Este pacote fornece uma JsonPatchDocument<TModel> classe para representar um documento de Patch JSON para objetos de tipo T e lógica personalizada para serializar e desserializar documentos de Patch JSON usando System.Text.Json. O método chave da JsonPatchDocument<TModel> classe é ApplyTo(Object), que aplica as operações de patch a um objeto de destino do tipo T.

Código do método de ação que aplica o patch JSON

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

Método de exemplo da ação do controlador:

[HttpPatch("{id}", Name = "UpdateCustomer")]
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    // Retrieve the customer by ID
    var customer = db.Customers.FirstOrDefault(c => c.Id == id);

    // Return 404 Not Found if customer doesn't exist
    if (customer == null)
    {
        return NotFound();
    }

    patchDoc.ApplyTo(customer, jsonPatchError =>
        {
            var key = jsonPatchError.AffectedObject.GetType().Name;
            ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
        }
    );

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

    return new ObjectResult(customer);
}

Esse código do aplicativo de exemplo funciona com o seguinte Customer e Order modelos:

namespace App.Models;

public class Customer
{
    public string Id { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
    public string? Address { get; set; }
    public List<Order>? Orders { get; set; }

    public Customer()
    {
        Id = Guid.NewGuid().ToString();
    }
}
namespace App.Models;

public class Order
{
    public string Id { get; set; }
    public DateTime? OrderDate { get; set; }
    public DateTime? ShipDate { get; set; }
    public decimal TotalAmount { get; set; }

    public Order()
    {
        Id = Guid.NewGuid().ToString();
    }
}

As principais etapas do método de ação de exemplo:

  • Recuperar o Cliente:
    • O método recupera um Customer objeto do banco de dados AppDb usando a ID fornecida.
    • Se nenhum Customer objeto for encontrado, ele retornará uma 404 Not Found resposta.
  • Aplicar patch JSON:
    • O ApplyTo(Object) método aplica as operações de patch JSON do patchDoc ao objeto recuperado Customer .
    • Se ocorrerem erros durante o aplicativo de patch, como operações ou conflitos inválidos, eles serão capturados por um delegado de tratamento de erros. Esse delegado adiciona mensagens de erro ao ModelState usando o nome do tipo do objeto afetado e a mensagem de erro.
  • Valide ModelState:
    • Depois de aplicar o patch, o método verifica se há erros em ModelState.
    • Se o ModelState valor for inválido, como devido a erros de patch, ele retornará uma 400 Bad Request resposta com os erros de validação.
  • Retorne o cliente atualizado:
    • Se o patch for aplicado com êxito e ModelState for válido, o método retornará o objeto atualizado Customer na resposta.

Exemplo de resposta de erro:

O exemplo a seguir mostra o corpo de uma 400 Bad Request resposta para uma operação de patch JSON quando o caminho especificado é inválido:

{
  "Customer": [
    "The target location specified by path segment 'foobar' was not found."
  ]
}

Aplicar um documento de patch JSON a um objeto

Os exemplos a seguir demonstram como usar o ApplyTo(Object) método para aplicar um documento de Patch JSON a um objeto.

Exemplo: aplicar um JsonPatchDocument<TModel> a um objeto

O exemplo a seguir demonstra:

  • As operações add, replace e remove.
  • Operações em propriedades aninhadas.
  • Adicionando um novo item a uma matriz.
  • Usando um conversor de enumeração de cadeia de caracteres JSON em um documento de patch JSON.
// Original object
var person = new Person {
    FirstName = "John",
    LastName = "Doe",
    Email = "johndoe@gmail.com",
    PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
    Address = new Address
    {
        Street = "123 Main St",
        City = "Anytown",
        State = "TX"
    }
};

// Raw JSON patch document
string jsonPatch = """
[
    { "op": "replace", "path": "/FirstName", "value": "Jane" },
    { "op": "remove", "path": "/Email"},
    { "op": "add", "path": "/Address/ZipCode", "value": "90210" },
    { "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "987-654-3210",
                                                                "Type": "Work" } }
]
""";

// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON patch document
patchDoc!.ApplyTo(person);

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

O exemplo anterior resulta na seguinte saída do objeto atualizado:

{
    "firstName": "Jane",
    "lastName": "Doe",
    "address": {
        "street": "123 Main St",
        "city": "Anytown",
        "state": "TX",
        "zipCode": "90210"
    },
    "phoneNumbers": [
        {
            "number": "123-456-7890",
            "type": "Mobile"
        },
        {
            "number": "987-654-3210",
            "type": "Work"
        }
    ]
}

O ApplyTo(Object) método geralmente segue as convenções e opções de System.Text.Json para processamento do JsonPatchDocument<TModel>, incluindo o comportamento controlado pelas seguintes opções:

Principais diferenças entre System.Text.Json e a nova JsonPatchDocument<TModel> implementação:

  • O tipo de tempo de execução do objeto de destino, não o tipo declarado, determina quais propriedades ApplyTo(Object) altera.
  • A desserialização System.Text.Json depende do tipo declarado para identificar as propriedades elegíveis.

Exemplo: aplicar um JsonPatchDocument com tratamento de erros

Há vários erros que podem ocorrer ao aplicar um documento de patch JSON. Por exemplo, o objeto de destino pode não ter a propriedade especificada ou o valor especificado pode ser incompatível com o tipo de propriedade.

O JSON Patch dá suporte à test operação, que verifica se um valor especificado é igual à propriedade de destino. Se isso não ocorrer, retornará um erro.

O exemplo a seguir demonstra como lidar com esses erros normalmente.

Importante

O objeto passado para o ApplyTo(Object) método é modificado no local. O chamador é responsável por descartar alterações se alguma operação falhar.

// Original object
var person = new Person {
    FirstName = "John",
    LastName = "Doe",
    Email = "johndoe@gmail.com"
};

// Raw JSON patch document
string jsonPatch = """
[
    { "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
    { "op": "test", "path": "/FirstName", "value": "Jane" },
    { "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";

// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
    {
        errors ??= new ();
        var key = jsonPatchError.AffectedObject.GetType().Name;
        if (!errors.ContainsKey(key))
        {
            errors.Add(key, new string[] { });
        }
        errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
    });
if (errors != null)
{
    // Print the errors
    foreach (var error in errors)
    {
        Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
    }
}

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

O exemplo anterior resulta na seguinte saída:

Error in Person: The current value 'John' at path 'FirstName' is not equal 
to the test value 'Jane'.
{
    "firstName": "John",
    "lastName": "Smith",              <<< Modified!
    "email": "janedoe@gmail.com",     <<< Modified!
    "phoneNumbers": []
}

Mitigação dos riscos à segurança

Ao usar o Microsoft.AspNetCore.JsonPatch.SystemTextJson pacote, é essencial entender e atenuar possíveis riscos de segurança. As seções a seguir descrevem os riscos de segurança identificados associados ao Patch JSON e fornecem mitigações recomendadas para garantir o uso seguro do pacote.

Importante

Essa não é uma lista completa de ameaças. Os desenvolvedores de aplicativos devem realizar suas próprias revisões de modelo de ameaças para determinar uma lista abrangente específica do aplicativo e criar mitigações apropriadas, conforme necessário. Por exemplo, os aplicativos que expõem coleções a operações de patch devem considerar o potencial de ataques de complexidade algorítmica se essas operações inserirem ou removerem elementos no início da coleção.

Para minimizar os riscos de segurança ao integrar a funcionalidade de Patch JSON em seus aplicativos, os desenvolvedores devem:

  • Execute modelos de ameaças abrangentes para seus próprios aplicativos.
  • Abordar ameaças identificadas.
  • Siga as mitigações recomendadas nas seções a seguir.

Negação de Serviço (DoS) por meio da amplificação de memória

  • Cenário: um cliente mal-intencionado envia uma copy operação que duplica grafos de objetos grandes várias vezes, levando ao consumo excessivo de memória.
  • Impacto: possíveis condições de Out-Of-Memory (OOM), causando interrupções no serviço.
  • Mitigação:
    • Valide os documentos JSON Patch de entrada quanto ao tamanho e à estrutura antes de chamar ApplyTo(Object).
    • A validação deve ser específica do aplicativo, mas uma validação de exemplo pode ser semelhante à seguinte:
public void Validate(JsonPatchDocument<T> patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app needs.
    if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
                              > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}

Subversão lógica de negócios

  • Cenário: as operações de patch podem manipular campos com invariáveis implícitas (por exemplo, sinalizadores internos, IDs ou campos computados), violando restrições comerciais.
  • Impacto: problemas de integridade de dados e comportamento de aplicativo não intencional.
  • Mitigação:
    • Use POCOs (Objetos CLR Antigos Simples) com propriedades definidas explicitamente que são seguras de modificar.
      • Evite expor propriedades confidenciais ou críticas à segurança no objeto de destino.
      • Se um objeto POCO não for usado, valide o objeto corrigido após a aplicação de operações para garantir que regras de negócios e invariáveis não sejam violados.

Autenticação e autorização

  • Cenário: clientes não autenticados ou não autorizados enviam solicitações de patch JSON mal-intencionadas.
  • Impacto: acesso não autorizado para modificar dados confidenciais ou interromper o comportamento do aplicativo.
  • Mitigação:
    • Proteja os endpoints que aceitam solicitações JSON Patch com mecanismos de autenticação e autorização adequados.
    • Restrinja o acesso a clientes confiáveis ou usuários com permissões apropriadas.

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 documento de JSON Patch da pasta de projeto JSON.

Recursos adicionais

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

Importante

O padrão de patch JSON tem riscos de segurança inerentes. Essa implementação não tenta atenuar esses riscos inerentes à segurança. É responsabilidade do desenvolvedor garantir que o documento do Patch JSON seja seguro para aplicar ao objeto de destino. Para obter mais informações, consulte a seção Mitigando riscos de segurança .

Instalação do pacote

O suporte ao patch JSON na API Web do ASP.NET Core é baseado em Newtonsoft.Json e precisa do pacote NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

Para habilitar o suporte ao patch JSON:

  • 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 todo o conteúdo JSON. 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 patch JSON ao usar System.Text.Json

O formatador de entrada baseado em System.Text.Json não dá suporte ao patch JSON. Para adicionar suporte ao patch JSON usando Newtonsoft.Json, enquanto os outros formatadores de entrada e saída permanecem inalterados:

  • 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 solicitações patch JSON.
  • 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 de 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 documentos JSON representam um recurso, um documento de JSON Patch para o recurso e o resultado da aplicação de operações patch.

Exemplo de recurso

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

Exemplo de JSON 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

Este é o recurso após a aplicação do documento JSON Patch anterior:

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

As alterações feitas ao aplicar um documento patch JSON 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 operações compatíveis conforme definido na especificação de 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.

Patch JSON em ASP.NET Core

A implementação do ASP.NET Core de JSON Patch é fornecida no pacote do 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 documento de JSON Patch da pasta de projeto JSON.

Mitigação dos riscos à segurança

Ao usar o pacote Microsoft.AspNetCore.JsonPatch com a implementação baseada em Newtonsoft.Json, é crítico entender e atenuar possíveis riscos de segurança. As seções a seguir descrevem os riscos de segurança identificados associados ao Patch JSON e fornecem mitigações recomendadas para garantir o uso seguro do pacote.

Importante

Essa não é uma lista completa de ameaças. Os desenvolvedores de aplicativos devem realizar suas próprias revisões de modelo de ameaças para determinar uma lista abrangente específica do aplicativo e criar mitigações apropriadas, conforme necessário. Por exemplo, os aplicativos que expõem coleções a operações de patch devem considerar o potencial de ataques de complexidade algorítmica se essas operações inserirem ou removerem elementos no início da coleção.

Ao executar modelos de ameaças abrangentes para seus próprios aplicativos e lidar com ameaças identificadas, seguindo as mitigações recomendadas abaixo, os consumidores desses pacotes podem integrar a funcionalidade do Patch JSON em seus aplicativos, minimizando os riscos de segurança.

Negação de Serviço (DoS) por meio da amplificação de memória

  • Cenário: um cliente mal-intencionado envia uma copy operação que duplica grafos de objetos grandes várias vezes, levando ao consumo excessivo de memória.
  • Impacto: possíveis condições de Out-Of-Memory (OOM), causando interrupções no serviço.
  • Mitigação:
    • Valide os documentos JSON Patch de entrada quanto ao tamanho e à estrutura antes de chamar ApplyTo.
    • A validação precisa ser específica do aplicativo, mas uma validação de exemplo pode ser semelhante à seguinte:
public void Validate(JsonPatchDocument patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app needs.
    if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
                              > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}

Subversão lógica de negócios

  • Cenário: as operações de patch podem manipular campos com invariáveis implícitas (por exemplo, sinalizadores internos, IDs ou campos computados), violando restrições comerciais.
  • Impacto: problemas de integridade de dados e comportamento de aplicativo não intencional.
  • Mitigação:
    • Use objetos POCO com propriedades definidas explicitamente que são seguras de modificar.
    • Evite expor propriedades confidenciais ou críticas à segurança no objeto de destino.
    • Se nenhum objeto POCO for usado, valide o objeto corrigido após a aplicação de operações para garantir que regras de negócios e invariáveis não sejam violados.

Autenticação e autorização

  • Cenário: clientes não autenticados ou não autorizados enviam solicitações de patch JSON mal-intencionadas.
  • Impacto: acesso não autorizado para modificar dados confidenciais ou interromper o comportamento do aplicativo.
  • Mitigação:
    • Proteja os endpoints que aceitam solicitações JSON Patch com mecanismos de autenticação e autorização adequados.
    • Restrinja o acesso a clientes confiáveis ou usuários com permissões apropriadas.

Recursos adicionais

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

Importante

O padrão de patch JSON tem riscos de segurança inerentes. Como esses riscos são inerentes ao padrão de Patch JSON, essa implementação não tenta reduzir os riscos inerentes à segurança. É responsabilidade do desenvolvedor garantir que o documento do Patch JSON seja seguro para aplicar ao objeto de destino. Para obter mais informações, consulte a seção Mitigando riscos de segurança .

Instalação do pacote

Para habilitar o suporte ao patch JSON no seu aplicativo, conclua as seguintes etapas:

  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:

Patch JSON, AddNewtonsoftJson e System.Text.Json

AddNewtonsoftJson substitui os formatadores padrão de entrada e saída baseados em System.Text.Json usados para formatar todo o conteúdo JSON. Para adicionar suporte ao patch JSON usando Newtonsoft.Json sem alterar os outros formatadores, 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 de 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 documentos JSON representam um recurso, um documento de JSON Patch para o recurso e o resultado da aplicação de operações patch.

Exemplo de recurso

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

Exemplo de JSON 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

Este é o recurso após a aplicação do documento JSON Patch anterior:

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

As alterações feitas ao aplicar um documento patch JSON 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 operações compatíveis conforme definido na especificação de 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.

Patch JSON em ASP.NET Core

A implementação do ASP.NET Core de JSON Patch é fornecida no pacote do 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 documento de JSON Patch da pasta de projeto JSON.