JsonPatch en la API web de ASP.NET Core

En este artículo, se explica cómo administrar solicitudes JSON Patch en una API web ASP.NET Core.

Instalación del paquete

La compatibilidad con JSON Patch en una API web ASP.NET Core se basa en Newtonsoft.Json y requiere el paquete de NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson. Para habilitar la compatibilidad con JSON Patch:

  • Instale el paquete NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Llame a AddNewtonsoftJson. Por ejemplo:

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

AddNewtonsoftJson reemplaza los formateadores de entrada y salida basados en System.Text.Json, que se usan para dar formato a todo el contenido JSON. Este método de extensión es compatible con los siguientes métodos de registro del servicio MVC:

JsonPatch requiere que el encabezado Content-Type se establezca en application/json-patch+json.

Agregar compatibilidad con JSON Patch al usar System.Text.Json

El formateador de entrada basado en System.Text.Json no admite JSON Patch. Para agregar compatibilidad con JSON Patch mediante Newtonsoft.Json, sin cambiar los demás formateadores de entrada y salida:

  • Instale el paquete NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

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

El código anterior crea una instancia de NewtonsoftJsonPatchInputFormatter y la inserta como la primera entrada de la colección MvcOptions.InputFormatters. Este orden de registro garantiza que:

  • NewtonsoftJsonPatchInputFormatter procese solicitudes JSON Patch.
  • Los formateadores de entradas existentes basados en System.Text.Json procesen todas las demás solicitudes y respuestas JSON.

Utilice el método Newtonsoft.Json.JsonConvert.SerializeObject para serializar un token de seguridad JsonPatchDocument.

Método de solicitud HTTP PATCH

Los métodos PUT y PATCH se usan para actualizar un recurso existente. La diferencia entre ellos es que PUT reemplaza el recurso entero, mientras que PATCH especifica únicamente los cambios.

JSON Patch

JSON Patch es un formato para especificar las actualizaciones que se aplicarán a un recurso. Un documento JSON Patch tiene una matriz de operaciones. Cada operación identifica un tipo determinado de cambio. Algunos ejemplos de estos cambios incluyen agregar un elemento de matriz o reemplazar un valor de propiedad.

Por ejemplo, los documentos JSON siguientes representan un recurso, un documento JSON Patch para el recurso y el resultado de aplicar las operaciones de actualización.

Ejemplo de recursos

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

Ejemplo de JSON Patch

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

En el código JSON anterior:

  • La propiedad op indica el tipo de operación.
  • La propiedad path indica el elemento que se va a actualizar.
  • La propiedad value proporciona el nuevo valor.

Recurso después de la revisión

Este es el recurso después de aplicar el documento JSON Patch anterior:

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

Los cambios realizados mediante la aplicación de un documento JSON Patch a un recurso son atómicos. Si cualquier operación en la lista falla, no se aplica ninguna operación de la lista.

Sintaxis de path

La propiedad path de un objeto de operación tiene barras inversas entre niveles. Por ejemplo, "/address/zipCode".

Para especificar elementos de matriz se usan índices de base cero. El primer elemento de la matriz addresses estaría en /addresses/0. Para usar add al final de una matriz, use un guion (-) en lugar de un número de índice: /addresses/-.

Operations

En la siguiente tabla, se muestran las operaciones admitidas, como se ha definido en la especificación de JSON Patch:

Operación Notas
add Agrega un elemento de propiedad o matriz. Para la propiedad existente: establece el valor.
remove Quita un elemento de propiedad o matriz.
replace Lo mismo que remove seguido de add en la misma ubicación.
move Lo mismo que remove desde el origen seguido de add al destino mediante el valor del origen.
copy Lo mismo que add al destino mediante el valor del origen.
test Devuelve el código de estado correcto si el valor en path = al value proporcionado.

JSON Patch en ASP.NET Core

La implementación de ASP.NET Core de JSON Patch se proporciona en el paquete de NuGet Microsoft.AspNetCore.JsonPatch.

Código del método de acción

En un controlador de API, un método de acción para JSON Patch:

Este es un ejemplo:

[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 de la aplicación de ejemplo funciona con el siguiente 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; }
}

El método de acción de ejemplo:

  • Construye un objeto Customer.
  • Aplica la revisión.
  • Devuelve el resultado en el cuerpo de la respuesta.

En una aplicación real, el código recuperaría los datos de un almacén como una base de datos y actualizaría la base de datos después de aplicar la revisión.

Estado del modelo

En el ejemplo anterior del método de acción, se llama a una sobrecarga de ApplyTo que toma el estado del modelo como uno de sus parámetros. Con esta opción, puede obtener mensajes de error en las respuestas. En el ejemplo siguiente se muestra el cuerpo de una respuesta 400 Solicitud incorrecta de una operación test:

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

Objetos dinámicos

En el ejemplo siguiente de método de acción, se muestra cómo aplicar una revisión a un objeto dinámico:

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

    return Ok(obj);
}

La operación add

  • Si path apunta a un elemento de matriz: inserta un nuevo elemento delante del especificado por path.
  • Si path apunta a una propiedad: establece el valor de la propiedad.
  • Si path apunta a una ubicación que no existe:
    • Si el recurso para revisar es un objeto dinámico: agrega una propiedad.
    • Si el recurso para revisar es un objeto estático: la solicitud produce un error.

El siguiente documento de revisión de ejemplo establece el valor de CustomerName y agrega un objeto Order al final de la matriz Orders.

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

La operación remove

  • Si path apunta a un elemento de matriz: quita el elemento.
  • Si path apunta a una propiedad:
    • Si el recurso para revisar es un objeto dinámico: quita la propiedad.
    • Si el recurso para revisar es un objeto estático:
      • Si la propiedad acepta valores NULL: la establece en null.
      • Si la propiedad es distinta de null, la establece en default<T>.

En el siguiente documento de revisión de ejemplo, se establece CustomerName en null y se elimina Orders[0]:

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

La operación replace

Esta operación es funcionalmente igual que remove seguida de add.

En el siguiente documento de revisión de ejemplo, se establece el valor de CustomerName y se reemplaza Orders[0] por un nuevo objeto Order:

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

La operación move

  • Si path apunta a un elemento de matriz: copia el elemento from en la ubicación del elemento path y, luego, ejecuta una operación remove en el elemento from.
  • Si path apunta a una propiedad: copia el valor de la propiedad from en la propiedad path y, luego, ejecuta la operación remove en la propiedad from.
  • Si path apunta a una propiedad que no existe:
    • Si el recurso para revisar es un objeto estático: la solicitud produce un error.
    • Si el recurso para revisar es un objeto dinámico: copia la propiedad from en la ubicación indicada por path y, luego, ejecuta una operación remove en la propiedad from.

En el siguiente documento de revisión de ejemplo:

  • Se copia el valor de Orders[0].OrderName en CustomerName.
  • Se establece Orders[0].OrderName en null.
  • Se mueve Orders[1] delante de Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

La operación copy

Esta operación es funcionalmente igual que la operación move sin el paso remove final.

En el siguiente documento de revisión de ejemplo:

  • Se copia el valor de Orders[0].OrderName en CustomerName.
  • Se inserta una copia de Orders[1] delante de Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

La operación test

Si el valor de la ubicación indicada por path es diferente del valor proporcionado en value, la solicitud produce un error. En ese caso, la solicitud PATCH entera produce un error incluso si todas las demás operaciones del documento de revisión se realizan correctamente.

La operación test se usa habitualmente para impedir una actualización cuando hay un conflicto de simultaneidad.

El siguiente documento de revisión de ejemplo no tiene ningún efecto si el valor inicial de CustomerName es "John", porque la prueba produce un error:

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

Obtención del código

Vea o descargue el código de ejemplo. (Método de descarga).

Para probar el ejemplo, ejecute la aplicación y envíe solicitudes HTTP con la configuración siguiente:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Método HTTP: PATCH
  • Encabezado: Content-Type: application/json-patch+json
  • Cuerpo: copie y pegue uno de los ejemplos de documento JSON Patch de la carpeta de proyecto JSON.

Recursos adicionales

En este artículo, se explica cómo administrar solicitudes JSON Patch en una API web ASP.NET Core.

Instalación del paquete

Para habilitar la compatibilidad con JSON Patch en la aplicación, complete los siguientes pasos:

  1. Instale el paquete NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  2. Actualice el método Startup.ConfigureServices del proyecto para llamar a AddNewtonsoftJson. Por ejemplo:

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson es compatible con los métodos de registro del servicio MVC:

JSON Patch, AddNewtonsoftJson y System.Text.Json

AddNewtonsoftJson reemplaza los formateadores de entrada y salida basados en System.Text.Json usados para formatear todo el contenido JSON. Para agregar compatibilidad con JSON Patch mediante Newtonsoft.Json, sin cambiar los otros formateadores, actualice el método Startup.ConfigureServices del proyecto, como se indica a continuación:

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

El código anterior requiere el paquete Microsoft.AspNetCore.Mvc.NewtonsoftJson y las siguientes instrucciones 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 el método Newtonsoft.Json.JsonConvert.SerializeObject para serializar un JsonPatchDocument.

Método de solicitud HTTP PATCH

Los métodos PUT y PATCH se usan para actualizar un recurso existente. La diferencia entre ellos es que PUT reemplaza el recurso entero, mientras que PATCH especifica únicamente los cambios.

JSON Patch

JSON Patch es un formato para especificar las actualizaciones que se aplicarán a un recurso. Un documento JSON Patch tiene una matriz de operaciones. Cada operación identifica un tipo determinado de cambio. Algunos ejemplos de estos cambios incluyen agregar un elemento de matriz o reemplazar un valor de propiedad.

Por ejemplo, los documentos JSON siguientes representan un recurso, un documento JSON Patch para el recurso y el resultado de aplicar las operaciones de actualización.

Ejemplo de recursos

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

Ejemplo de JSON Patch

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

En el código JSON anterior:

  • La propiedad op indica el tipo de operación.
  • La propiedad path indica el elemento que se va a actualizar.
  • La propiedad value proporciona el nuevo valor.

Recurso después de la revisión

Este es el recurso después de aplicar el documento JSON Patch anterior:

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

Los cambios realizados mediante la aplicación de un documento JSON Patch a un recurso son atómicos. Si cualquier operación en la lista falla, no se aplica ninguna operación de la lista.

Sintaxis de path

La propiedad path de un objeto de operación tiene barras inversas entre niveles. Por ejemplo, "/address/zipCode".

Para especificar elementos de matriz se usan índices de base cero. El primer elemento de la matriz addresses estaría en /addresses/0. Para usar add al final de una matriz, use un guion (-) en lugar de un número de índice: /addresses/-.

Operations

En la siguiente tabla, se muestran las operaciones admitidas, como se ha definido en la especificación de JSON Patch:

Operación Notas
add Agrega un elemento de propiedad o matriz. Para la propiedad existente: establece el valor.
remove Quita un elemento de propiedad o matriz.
replace Lo mismo que remove seguido de add en la misma ubicación.
move Lo mismo que remove desde el origen seguido de add al destino mediante el valor del origen.
copy Lo mismo que add al destino mediante el valor del origen.
test Devuelve el código de estado correcto si el valor en path = al value proporcionado.

JSON Patch en ASP.NET Core

La implementación de ASP.NET Core de JSON Patch se proporciona en el paquete de NuGet Microsoft.AspNetCore.JsonPatch.

Código del método de acción

En un controlador de API, un método de acción para JSON Patch:

  • Se anota con el atributo HttpPatch.
  • Acepta JsonPatchDocument<T>, normalmente con [FromBody].
  • Llama a ApplyTo en el documento de revisión para aplicar los cambios.

Este es un ejemplo:

[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 de la aplicación de ejemplo funciona con el siguiente 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; }
    }
}

El método de acción de ejemplo:

  • Construye un objeto Customer.
  • Aplica la revisión.
  • Devuelve el resultado en el cuerpo de la respuesta.

En una aplicación real, el código recuperaría los datos de un almacén como una base de datos y actualizaría la base de datos después de aplicar la revisión.

Estado del modelo

En el ejemplo anterior del método de acción, se llama a una sobrecarga de ApplyTo que toma el estado del modelo como uno de sus parámetros. Con esta opción, puede obtener mensajes de error en las respuestas. En el ejemplo siguiente se muestra el cuerpo de una respuesta 400 Solicitud incorrecta de una operación test:

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

Objetos dinámicos

En el ejemplo siguiente de método de acción, se muestra cómo aplicar una revisión a un objeto dinámico:

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

    return Ok(obj);
}

La operación add

  • Si path apunta a un elemento de matriz: inserta un nuevo elemento delante del especificado por path.
  • Si path apunta a una propiedad: establece el valor de la propiedad.
  • Si path apunta a una ubicación que no existe:
    • Si el recurso para revisar es un objeto dinámico: agrega una propiedad.
    • Si el recurso para revisar es un objeto estático: la solicitud produce un error.

El siguiente documento de revisión de ejemplo establece el valor de CustomerName y agrega un objeto Order al final de la matriz Orders.

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

La operación remove

  • Si path apunta a un elemento de matriz: quita el elemento.
  • Si path apunta a una propiedad:
    • Si el recurso para revisar es un objeto dinámico: quita la propiedad.
    • Si el recurso para revisar es un objeto estático:
      • Si la propiedad acepta valores NULL: la establece en null.
      • Si la propiedad es distinta de null, la establece en default<T>.

En el siguiente documento de revisión de ejemplo, se establece CustomerName en null y se elimina Orders[0]:

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

La operación replace

Esta operación es funcionalmente igual que remove seguida de add.

En el siguiente documento de revisión de ejemplo, se establece el valor de CustomerName y se reemplaza Orders[0] por un nuevo objeto Order:

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

La operación move

  • Si path apunta a un elemento de matriz: copia el elemento from en la ubicación del elemento path y, luego, ejecuta una operación remove en el elemento from.
  • Si path apunta a una propiedad: copia el valor de la propiedad from en la propiedad path y, luego, ejecuta la operación remove en la propiedad from.
  • Si path apunta a una propiedad que no existe:
    • Si el recurso para revisar es un objeto estático: la solicitud produce un error.
    • Si el recurso para revisar es un objeto dinámico: copia la propiedad from en la ubicación indicada por path y, luego, ejecuta una operación remove en la propiedad from.

En el siguiente documento de revisión de ejemplo:

  • Se copia el valor de Orders[0].OrderName en CustomerName.
  • Se establece Orders[0].OrderName en null.
  • Se mueve Orders[1] delante de Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

La operación copy

Esta operación es funcionalmente igual que la operación move sin el paso remove final.

En el siguiente documento de revisión de ejemplo:

  • Se copia el valor de Orders[0].OrderName en CustomerName.
  • Se inserta una copia de Orders[1] delante de Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

La operación test

Si el valor de la ubicación indicada por path es diferente del valor proporcionado en value, la solicitud produce un error. En ese caso, la solicitud PATCH entera produce un error incluso si todas las demás operaciones del documento de revisión se realizan correctamente.

La operación test se usa habitualmente para impedir una actualización cuando hay un conflicto de simultaneidad.

El siguiente documento de revisión de ejemplo no tiene ningún efecto si el valor inicial de CustomerName es "John", porque la prueba produce un error:

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

Obtención del código

Vea o descargue el código de ejemplo. (Método de descarga).

Para probar el ejemplo, ejecute la aplicación y envíe solicitudes HTTP con la configuración siguiente:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Método HTTP: PATCH
  • Encabezado: Content-Type: application/json-patch+json
  • Cuerpo: copie y pegue uno de los ejemplos de documento JSON Patch de la carpeta de proyecto JSON.

Recursos adicionales