Partilhar via


Suporte a patch JSON na API Web ASP.NET Core

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

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

O que é o padrão JSON Patch?

O padrão JSON Patch:

  • É um formato padrão para descrever 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 é comumente 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 patches reduz o tamanho do payload e melhora a eficiência.

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

Suporte a patch JSON na API Web ASP.NET Core

O suporte ao patch JSON na API da Web ASP.NET Core é baseado na System.Text.Json serialização, começando com o .NET 10, implementando Microsoft.AspNetCore.JsonPatch com base na System.Text.Json serialização. Este recurso:

  • Requer o Microsoft.AspNetCore.JsonPatch.SystemTextJson pacote NuGet.
  • Alinha-se com as práticas modernas do .NET aproveitando a System.Text.Json biblioteca, que é otimizada para .NET.
  • Fornece melhor desempenho e uso reduzido de memória em comparação com a implementação baseada em Newtonsoft.Json de legado. Para obter mais informações sobre a implementação baseada em herdado Newtonsoft.Json, consulte a versão .NET 9 deste artigo.

Note

A implementação de Microsoft.AspNetCore.JsonPatch baseada em System.Text.Json não é um substituto imediato para a implementação baseada em Newtonsoft.Json. Não suporta tipos dinâmicos, por exemplo ExpandoObject.

Important

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

Habilite o suporte a patches JSON com System.Text.Json

Para habilitar o suporte ao 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 do 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 aplicando JSON Patch

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

Exemplo de método de 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);
}

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

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

  • Recupere 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.
  • Aplique o patch JSON:
    • O ApplyTo(Object) método aplica as operações JSON Patch do patchDoc ao objeto recuperado Customer .
    • Se ocorrerem erros durante o aplicativo de patch, como operações inválidas ou conflitos, eles serão capturados por um delegado de tratamento de erros. Este delegado adiciona mensagens de erro ao ModelState usando o nome do tipo do objeto afetado e a mensagem de erro.
  • Validar ModelState:
    • Depois de aplicar o patch, o método verifica ModelState para erros.
    • Se o ModelState 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 o 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 a JsonPatchDocument<TModel> a um objeto

O exemplo a seguir demonstra:

  • As operações add, replace e remove.
  • Operações em propriedades aninhadas.
  • Adicionar um novo item a uma matriz.
  • Usando um conversor JSON String Enum 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 processar o 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) são corrigidas.
  • System.Text.Json A desserialização depende do tipo declarado para identificar 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.

JSON Patch suporta a test operação, que verifica se um valor especificado é igual à propriedade de destino. Se isso não acontecer, ele retornará um erro.

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

Important

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

// 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 de segurança

Ao usar o Microsoft.AspNetCore.JsonPatch.SystemTextJson pacote, é fundamental entender e mitigar 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.

Important

Esta não é uma lista exaustiva de ameaças. Os desenvolvedores de aplicativos devem conduzir suas próprias revisões de modelo de ameaça 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 JSON Patch em seus aplicativos, os desenvolvedores devem:

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

Negação de Serviço (DoS) via amplificação de memória

  • Cenário: Um cliente mal-intencionado envia uma copy operação que duplica gráficos de objetos grandes várias vezes, levando ao consumo excessivo de memória.
  • Impacto: condições potenciais de Out-Of-Memory (OOM), causando interrupções no serviço.
  • Mitigation:
    • Valide os documentos de patch JSON recebidos quanto ao tamanho e estrutura antes de chamar ApplyTo(Object).
    • A validação deve ser específica do aplicativo, mas um exemplo de validação pode ser semelhante ao 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 da lógica de negócios

  • Cenário: as operações de patch podem manipular campos com invariantes implícitos (por exemplo, sinalizadores internos, IDs ou campos computados), violando restrições de negócios.
  • Impacto: problemas de integridade de dados e comportamento não intencional do aplicativo.
  • Mitigation:
    • Use POCOs (Plain Old CLR Objects) com propriedades explicitamente definidas que são seguras para modificar.
      • Evite expor propriedades confidenciais ou críticas de segurança no objeto de destino.
      • Se um objeto POCO não for usado, valide o objeto corrigido depois de aplicar operações para garantir que as regras de negócios e invariantes não sejam violadas.

Autenticação e autorização

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

Obter o código

Ver e descarregar código de exemplo. (Como fazer o download).

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

  • Endereço 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 patch JSON da pasta do projeto JSON .

Recursos adicionais

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

Important

O padrão JSON Patch tem riscos de segurança inerentes. Essa implementação não tenta mitigar esses riscos de segurança inerentes. É responsabilidade do desenvolvedor garantir que o documento do patch JSON seja seguro para ser aplicado 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 ASP.NET Core é baseado em Newtonsoft.Json e requer o pacote NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

Para habilitar o suporte ao patch JSON:

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

  • Ligue para 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 de entrada e saída padrão baseados em System.Text.Jsonusados para formatar todo o conteúdo JSON . Esse método de extensão é compatível com os seguintes métodos de registro de serviço MVC:

JsonPatch requer a configuração do cabeçalho Content-Type para application/json-patch+json.

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

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

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

  • Atualização 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. Esta ordem de registo garante que:

  • NewtonsoftJsonPatchInputFormatter processa solicitações de patch JSON.
  • As entradas e formatters existentes baseadas em System.Text.Jsonprocessam 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 todo o recurso, enquanto PATCH especifica apenas as alterações.

JSON Patch (um padrão para manipulação de dados JSON)

JSON Patch é um formato para especificar atualizações a serem aplicadas a um recurso. Um documento de patch JSON tem uma matriz de operações . Cada operação identifica um tipo específico de alteração. Exemplos de tais 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 JSON Patch para o recurso e o resultado da aplicação das operações Patch.

Exemplo de recurso

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

Exemplo de patch JSON

[
  {
    "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 após patch

Aqui está o recurso depois de aplicar o documento de patch JSON 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 documento de patch JSON a um recurso são atômicas. Se alguma operação na lista falhar, nenhuma operação na lista será aplicada.

Sintaxe do caminho

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

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

Operations

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

Operation Notes
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 de add para o destino usando o valor da origem.
copy O mesmo que add em direção ao destino, utilizando o valor da origem.
test Retornar o código de status de sucesso se o valor em path for igual ao fornecido em value.

JSON Patch no ASP.NET Core

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

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

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

Aqui está 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);
    }
}

Este código do aplicativo de exemplo funciona com o seguinte modelo de 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 método de exemplo da ação:

  • Constrói um Customer.
  • Aplica a correção.
  • Devolve o resultado no corpo da resposta.

Em um aplicativo real, o código recuperaria os dados de um armazenamento, como um banco de dados, e atualizaria o banco de dados depois de aplicar o patch.

Estado do modelo

O exemplo de método de ação anterior invoca uma sobrecarga de ApplyTo que utiliza o estado do modelo como um dos 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 Bad Request para uma operação test:

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

Objetos dinâmicos

O exemplo de 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 de adição

  • Se path apontar para um elemento de matriz: insere novo elemento antes do especificado por path.
  • Caso path aponte para uma propriedade: defini o valor da propriedade.
  • Se path apontar para um local inexistente:
    • Se o recurso a ser corrigido for um objeto dinâmico: adiciona uma propriedade.
    • Se o recurso a ser corrigido for um objeto estático: a solicitação falhará.

O documento de patch de exemplo 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 de remoção

  • Se path aponta para um elemento da matriz, remove o elemento.
  • Caso path aponte para uma propriedade:
    • Se o recurso a ser corrigido for um objeto dinâmico: remove a propriedade.
    • Se o recurso a ser corrigido for um objeto estático:
      • Se a propriedade for anulável: define-a como null.
      • Se a propriedade não for anulável, define-a como default<T>.

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

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

A operação de substituição

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

O documento de patch de exemplo 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 de mudança

  • Se path apontar para um elemento de matriz: copia o elemento from para o local do elemento path e, em seguida, executa uma operação de remove no elemento from.
  • Se path apontar para uma propriedade, copia o valor da propriedade from para a propriedade path e, em seguida, executa uma operação remove na propriedade from.
  • Se path apontar para um imóvel inexistente:
    • Se o recurso a ser corrigido for um objeto estático: a solicitação falhará.
    • Se o recurso a ser corrigido for um objeto dinâmico: copia from propriedade para o local indicado por pathe, em seguida, executa uma operação remove na propriedade from.

O seguinte documento de patch de exemplo:

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

A operação de cópia

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

O seguinte documento de patch de exemplo:

  • 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 de teste

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 PATCH falha, mesmo que todas as outras operações no documento de patch sejam bem-sucedidas.

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

O seguinte documento de patch de exemplo não terá 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

Ver e descarregar código de exemplo. (Como fazer o download).

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

  • Endereço 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 patch JSON da pasta do projeto JSON .

Mitigação dos riscos de segurança

Ao usar o pacote Microsoft.AspNetCore.JsonPatch com a implementação baseada em Newtonsoft.Json, é fundamental entender e mitigar 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.

Important

Esta não é uma lista exaustiva de ameaças. Os desenvolvedores de aplicativos devem conduzir suas próprias revisões de modelo de ameaça 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 enquanto seguem as mitigações recomendadas abaixo, os consumidores desses pacotes podem integrar a funcionalidade JSON Patch em seus aplicativos, minimizando os riscos de segurança.

Negação de Serviço (DoS) via amplificação de memória

  • Cenário: Um cliente mal-intencionado envia uma copy operação que duplica gráficos de objetos grandes várias vezes, levando ao consumo excessivo de memória.
  • Impacto: condições potenciais de Out-Of-Memory (OOM), causando interrupções no serviço.
  • Mitigation:
    • Valide os documentos de patch JSON recebidos quanto ao tamanho e estrutura antes de chamar ApplyTo.
    • A validação precisa ser específica do aplicativo, mas um exemplo de validação pode ser semelhante ao 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 da lógica de negócios

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

Autenticação e autorização

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

Recursos adicionais

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

Important

O padrão JSON Patch tem riscos de segurança inerentes. Como esses riscos são inerentes ao padrão JSON Patch, essa implementação não tenta mitigar os riscos de segurança inerentes. É responsabilidade do desenvolvedor garantir que o documento do patch JSON seja seguro para ser aplicado 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 em seu aplicativo, conclua as seguintes etapas:

  1. Instale o pacote 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 a entrada e saída baseadas em System.Text.Jsonpara assuntos usados para formatação todo o conteúdo JSON. Para adicionar suporte para JSON Patch usando Newtonsoft.Json, deixando os outros formatters inalterados, atualize o método Startup.ConfigureServices do projeto da seguinte maneira:

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 requer 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 todo o recurso, enquanto PATCH especifica apenas as alterações.

JSON Patch (um padrão para manipulação de dados JSON)

JSON Patch é um formato para especificar atualizações a serem aplicadas a um recurso. Um documento de patch JSON tem uma matriz de operações . Cada operação identifica um tipo específico de alteração. Exemplos de tais 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 JSON Patch para o recurso e o resultado da aplicação das operações Patch.

Exemplo de recurso

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

Exemplo de patch JSON

[
  {
    "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 após patch

Aqui está o recurso depois de aplicar o documento de patch JSON 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 documento de patch JSON a um recurso são atômicas. Se alguma operação na lista falhar, nenhuma operação na lista será aplicada.

Sintaxe do caminho

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

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

Operations

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

Operation Notes
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 de add para o destino usando o valor da origem.
copy O mesmo que add em direção ao destino, utilizando o valor da origem.
test Retornar o código de status de sucesso se o valor em path for igual ao fornecido em value.

JSON Patch no ASP.NET Core

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

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

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

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

Aqui está 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);
    }
}

Este código do aplicativo de exemplo funciona com o seguinte modelo de 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 método de exemplo da ação:

  • Constrói um Customer.
  • Aplica a correção.
  • Devolve o resultado no corpo da resposta.

Em um aplicativo real, o código recuperaria os dados de um armazenamento, como um banco de dados, e atualizaria o banco de dados depois de aplicar o patch.

Estado do modelo

O exemplo de método de ação anterior invoca uma sobrecarga de ApplyTo que utiliza o estado do modelo como um dos 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 Bad Request 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 de 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 de adição

  • Se path apontar para um elemento de matriz: insere novo elemento antes do especificado por path.
  • Caso path aponte para uma propriedade: defini o valor da propriedade.
  • Se path apontar para um local inexistente:
    • Se o recurso a ser corrigido for um objeto dinâmico: adiciona uma propriedade.
    • Se o recurso a ser corrigido for um objeto estático: a solicitação falhará.

O documento de patch de exemplo 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 de remoção

  • Se path aponta para um elemento da matriz, remove o elemento.
  • Caso path aponte para uma propriedade:
    • Se o recurso a ser corrigido for um objeto dinâmico: remove a propriedade.
    • Se o recurso a ser corrigido for um objeto estático:
      • Se a propriedade for anulável: define-a como null.
      • Se a propriedade não for anulável, define-a como default<T>.

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

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

A operação de substituição

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

O documento de patch de exemplo 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 de mudança

  • Se path apontar para um elemento de matriz: copia o elemento from para o local do elemento path e, em seguida, executa uma operação de remove no elemento from.
  • Se path apontar para uma propriedade, copia o valor da propriedade from para a propriedade path e, em seguida, executa uma operação remove na propriedade from.
  • Se path apontar para um imóvel inexistente:
    • Se o recurso a ser corrigido for um objeto estático: a solicitação falhará.
    • Se o recurso a ser corrigido for um objeto dinâmico: copia from propriedade para o local indicado por pathe, em seguida, executa uma operação remove na propriedade from.

O seguinte documento de patch de exemplo:

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

A operação de cópia

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

O seguinte documento de patch de exemplo:

  • 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 de teste

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 PATCH falha, mesmo que todas as outras operações no documento de patch sejam bem-sucedidas.

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

O seguinte documento de patch de exemplo não terá 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

Ver e descarregar código de exemplo. (Como fazer o download).

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

  • Endereço 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 patch JSON da pasta do projeto JSON .