JsonPatch en la API web de ASP.NET Core

Por Tom Dykstra y Kirk Larkin

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 la revisión JSON en la aplicación, complete los pasos siguientes:

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

  2. Actualice el método del Startup.ConfigureServices 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.Jsen

AddNewtonsoftJson reemplaza los System.Text.Json formateadores de entrada y salida basados en que se usan para dar formato a todo el contenido JSON. Para agregar compatibilidad con json patch mediante , mientras se dejan los demás formateadores sin cambios, actualice el método del Newtonsoft.Json proyecto de la manera Startup.ConfigureServices siguiente:

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 Microsoft.AspNetCore.Mvc.NewtonsoftJson paquete y las siguientes using instrucciones:

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 Newtonsoft.Json.JsonConvert.SerializeObject método para serializar 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 son agregar un elemento de matriz o reemplazar un valor de propiedad.

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

Ejemplo de recursos

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

Ejemplo de revisión de JSON

[
  {
    "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 al aplicar un documento de revisión JSON a un recurso son atómicos. Si se produce un error en cualquier operación de la lista, 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 add al final de una matriz, use un guion ( ) en lugar de un número de - índice: /addresses/- .

Operaciones

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.

Revisión json en ASP.NET Core

La implementación de ASP.NET Core de JSON Patch se proporciona en el paquete 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.

Veamos 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 Customer modelo:

public class Customer
{
    public string CustomerName { get; set; }
    public List<Order> Orders { get; set; }
}
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 siguiente ejemplo 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>.

El siguiente documento de revisión de ejemplo CustomerName establece en null y 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 CustomerName de y se reemplaza por un nuevo objeto Orders[0] 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:

  • Dirección 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 documentos de revisión JSON de la carpeta del proyecto JSON.

Recursos adicionales

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

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 determinado tipo de cambio, como agregar un elemento de matriz o reemplazar un valor de propiedad.

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

Ejemplo de recursos

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

Ejemplo de revisión de JSON

[
  {
    "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 alguna operación de la lista produce un error, 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/-.

Operaciones

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.

JsonPatch en ASP.NET Core

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

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.

Veamos 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.

public class Customer
{
    public string CustomerName { get; set; }
    public List<Order> Orders { get; set; }
}
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:

  • Dirección 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 documentos de revisión JSON de la carpeta del proyecto JSON.

Recursos adicionales