Поддержка исправлений JSON в веб-API ASP.NET Core

В этой статье описывается обработка запросов JSON Patch в веб-API ASP.NET Core.

Поддержка исправлений JSON в веб-API ASP.NET Core основана на System.Text.Json сериализации и требует Microsoft.AspNetCore.JsonPatch.SystemTextJson пакета NuGet.

Что такое стандарт JSON Patch?

Стандарт патча JSON:

  • Стандартный формат описания изменений, применяемых к документу JSON.

  • Определяется в RFC 6902 и широко используется в API RESTful для выполнения частичных обновлений ресурсов JSON.

  • Описывает последовательность операций, которые изменяют документ JSON, например:

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

В веб-приложениях JSON Patch обычно используется в операции PATCH для частичного обновления ресурса. Вместо отправки всего ресурса для обновления клиенты могут отправлять документ исправления JSON, содержащий только изменения. Установка исправлений уменьшает размер полезной нагрузки и повышает эффективность.

Общие сведения о стандарте исправлений JSON см. в jsonpatch.com.

Поддержка исправлений JSON в веб-API ASP.NET Core

Поддержка JSON Patch в веб-API ASP.NET Core, начиная с .NET 10, основана на сериализации System.Text.Json. Он реализует Microsoft.AspNetCore.JsonPatch на основе сериализации System.Text.Json. Эта функция:

  • Требуется Microsoft.AspNetCore.JsonPatch.SystemTextJson пакет NuGet.
  • Соответствует современным методикам .NET, используя библиотеку System.Text.Json , оптимизированную для .NET.
  • Обеспечивает улучшенную производительность и сокращение использования памяти по сравнению с устаревшей реализацией Newtonsoft.Json. Дополнительные сведения о устаревшей Newtonsoft.Jsonреализации см. в версии .NET 9 этой статьи.

Note

Реализация Microsoft.AspNetCore.JsonPatch, основанная на сериализации System.Text.Json, не является заменой для устаревшей реализации на основе Newtonsoft.Json. Он не поддерживает динамические типы, например ExpandoObject.

Important

Стандарт JSON Patch имеет присущие риски безопасности. Так как эти риски присущи стандарту исправлений JSON, реализация ASP.NET Core не пытается устранить связанные с ней риски безопасности. Разработчик отвечает за безопасное применение документа исправления JSON к целевому объекту. Дополнительные сведения см. в разделе "Устранение рисков безопасности ".

Включение поддержки исправлений JSON с помощью System.Text.Json

Чтобы включить поддержку System.Text.Json JSON-патчей, установите пакет NuGet Microsoft.AspNetCore.JsonPatch.SystemTextJson.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson

Этот пакет предоставляет JsonPatchDocument<TModel> класс для представления документа JSON Patch для объектов типа TModel и пользовательскую логику для сериализации и десериализации JSON Patch документов с помощью System.Text.Json. Ключевой метод класса JsonPatchDocument<TModel> — это ApplyTo(Object), который применяет операции патчей к целевому объекту типа TModel.

Минимальная конечная точка API PATCH с применением JSON Patch

В минимальном API конечная точка PATCH для исправления JSON:

  • Используется MapPatch для определения маршрута.
  • Принимает JsonPatchDocument<TModel> параметр.
  • Вызывает ApplyTo(Object) для документа исправления, чтобы применить изменения.

Пример PATCH-конечной точки Minimal API

group.MapPatch("/{id}", async Task<Results<Ok<Customer>,ValidationProblem,NotFound<ProblemDetails>>> (AppDb db, string id,
    JsonPatchDocument<Customer> patchDoc) =>
{
    var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
    if (customer is null)
    {
        return TypedResults.NotFound<ProblemDetails>(new ());
    }
    if (patchDoc != null)
    {
        Dictionary<string, string[]>? errors = null;
        patchDoc.ApplyTo(customer, 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)
        {
            return TypedResults.ValidationProblem(errors);
        }

        // Only save if there are no errors
        await db.SaveChangesAsync();
    }

    return TypedResults.Ok(customer);
})
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");

Этот код из примера приложения работает со следующими Customer моделями и Order моделями:

using System.ComponentModel.DataAnnotations;

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

Ключевые шаги для примера конечной точки PATCH:

  • Извлечение клиента:
    • Эта конечная точка извлекает объект Customer из базы данных AppDb, используя предоставленный id.
    • Если объект Customer не найден, возвращается ответ 404 Not Found через TypedResults.NotFound().
  • Применение JSON-патча:
    • Метод ApplyTo(Object) применяет к полученному объекту Customer операции JSON Patch из patchDoc.
    • Если во время приложения исправления возникают ошибки, такие как недопустимые операции или конфликты, делегат обработки ошибок фиксирует их. Этот делегат собирает сообщения об ошибках в словарь, где ключом служит имя типа затронутого объекта.
  • Возвращать ошибки проверки:
    • Если делегат обработки ошибок обнаруживает какие-либо ошибки при применении исправления, конечный объект возвращает ответ ValidationProblem, содержащий сведения об ошибках через TypedResults.ValidationProblem(errors).
  • Сохраните и возвратите обновлённого клиента:
    • Если исправление успешно применено без ошибок, изменения сохраняются в базе данных, а конечная точка возвращает обновленный Customer объект через TypedResults.Ok(customer).

Пример ответа об ошибке

В следующем примере показано тело ответа с описанием ошибки проверки для операции JSON Patch, когда указанный путь недопустим:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Customer": [
      "The target location specified by path segment 'foobar' was not found."
    ]
  }
}

Применение документа JSON Patch к объекту

В следующих примерах показано, как использовать ApplyTo(Object) метод для применения документа JSON Patch к объекту.

Пример: применение JsonPatchDocument<TModel> к объекту

В следующем примере показано:

  • Операции add, replace и remove.
  • Операции с вложенными свойствами.
  • Добавление нового элемента в массив.
  • Использование преобразователя строкового перечисления JSON в документе 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));

В предыдущем примере приводятся следующие выходные данные обновленного объекта:

{
    "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"
        }
    ]
}

Метод ApplyTo(Object) обычно следует соглашениям и параметрам System.Text.Json обработки JsonPatchDocument<TModel>, включая поведение, управляемое следующими параметрами:

  • JsonNumberHandling: считываются ли числовые свойства из строк.
  • PropertyNameCaseInsensitive: Являются ли имена свойств чувствительными к регистру.

Основные различия между System.Text.Json и новой JsonPatchDocument<TModel> реализацией:

  • Тип среды выполнения целевого объекта, а не объявленного типа, определяет, какие свойства ApplyTo(Object) патчи.
  • System.Text.Json десериализация зависит от объявленного типа для определения подходящих свойств.

Пример. Применение JsonPatchDocument с обработкой ошибок

При применении документа исправления JSON могут возникать различные ошибки. Например, целевой объект может не иметь указанного свойства, или указанное значение может быть несовместимо с типом свойства.

JSON Patch поддерживает test операцию, которая проверяет, равно ли указанное значение целевому свойству. Если это не так, возвращается ошибка.

В следующем примере показано, как корректно обрабатывать эти ошибки.

Important

Объект, переданный методу ApplyTo(Object) , изменяется на месте. Вызывающий отвечает за отмену изменений, если какая-либо операция завершается ошибкой.

// 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));

В предыдущем примере приводятся следующие выходные данные:

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": []
}

Снижение рисков безопасности

При использовании Microsoft.AspNetCore.JsonPatch.SystemTextJson пакета важно понимать и устранять потенциальные риски безопасности. В следующих разделах описаны выявленные риски безопасности, связанные с исправлением JSON, и рекомендуемые способы устранения рисков для обеспечения безопасного использования пакета.

Important

Это не исчерпывающий список угроз. Разработчики приложений должны проводить собственные проверки модели угроз, чтобы определить полный список конкретных приложений и при необходимости придумать соответствующие меры по устранению рисков. Например, приложения, которые делают коллекции доступными для операций исправления, должны учитывать потенциал для атак алгоритмической сложности, если эти операции вставляют или удаляют элементы в начале коллекции.

Чтобы свести к минимуму риски безопасности при интеграции функций исправлений JSON в свои приложения, разработчики должны:

  • Проведение комплексных моделей угроз для их собственных приложений.
  • Устранение выявленных угроз.
  • Следуйте рекомендациям по устранению рисков в следующих разделах.

Отказ в обслуживании (DoS) через увеличение памяти

  • Сценарий: вредоносный клиент отправляет copy операцию, которая дублирует графы больших объектов несколько раз, что приводит к чрезмерному потреблению памяти.
  • Влияние: потенциальные условия исчерпанияOf-Memory (OOM), приводящие к сбоям в работе службы.
  • Mitigation:
    • Перед вызовом ApplyTo(Object) проверьте входящие JSON-патчи на соответствие размера и структуры.
    • Проверка должна быть специфичной для приложения, но пример проверки может выглядеть следующим образом:
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();
    }
}

Подверсия бизнес-логики

  • Сценарий. Операции исправления могут управлять полями с неявными инвариантными (например, внутренними флагами, идентификаторами или вычисляемыми полями), нарушая бизнес-ограничения.
  • Влияние: проблемы целостности данных и непреднамеренное поведение приложения.
  • Mitigation:
    • Используйте POCOs (простые объекты CLR) с явно определёнными свойствами, которые безопасно изменять.
      • Избегайте предоставления конфиденциальных или критически важных свойств безопасности в целевом объекте.
      • Если объект POCO не используется, проверьте исправленный объект после применения операций, чтобы убедиться, что бизнес-правила и инварианты не нарушаются.

Проверка подлинности и авторизация

  • Сценарий. Неавторизованные или несанкционированные клиенты отправляют вредоносные запросы на исправление JSON.
  • Влияние: несанкционированный доступ к изменению конфиденциальных данных или нарушению поведения приложения.
  • Mitigation:
    • Защита конечных точек, которые принимают запросы на исправление JSON с помощью надлежащих механизмов проверки подлинности и авторизации.
    • Ограничить доступ к доверенным клиентам или пользователям с соответствующими разрешениями.

Получение кода

Просмотреть или скачать образец кода. (Инструкция по скачиванию.)

Чтобы протестировать пример, запустите приложение и отправьте HTTP-запросы с помощью включенного .http файла.

Дополнительные ресурсы

В этой статье описывается обработка запросов JSON Patch в веб-API ASP.NET Core.

Important

Стандарт JSON Patch имеет присущие риски безопасности. Эта реализация не пытается устранить эти риски безопасности. Разработчик отвечает за безопасное применение документа исправления JSON к целевому объекту. Дополнительные сведения см. в разделе "Устранение рисков безопасности ".

Установка пакета

Поддержка JSON Patch в веб-API ASP.NET Core основана на Newtonsoft.Json и требует пакет NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

Чтобы включить поддержку исправлений JSON:

  • Установите пакет NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Вызовите AddNewtonsoftJson. Рассмотрим пример.

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

AddNewtonsoftJson заменяет входные и выходные средства форматирования на основе по умолчанию System.Text.Json, используемые для форматирования всего содержимого JSON. Этот метод расширения совместим со следующими методами регистрации службы MVC:

JsonPatch требует установки заголовка Content-Type в значение application/json-patch+json.

Добавьте поддержку JSON Patch при использовании System.Text.Json

Форматировщик входных данных на основе System.Text.Json не поддерживает JSON Patch. Чтобы добавить поддержку JSON Patch с помощью Newtonsoft.Json, при этом оставляя другие входные и выходные средства форматирования без изменений:

  • Установите пакет NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Обновить 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();
        }
    }
    

Предыдущий код создает экземпляр NewtonsoftJsonPatchInputFormatter и вставляет его в качестве первой записи в коллекцию MvcOptions.InputFormatters . Этот порядок регистрации гарантирует, что:

  • NewtonsoftJsonPatchInputFormatter обрабатывает запросы на исправление JSON.
  • Существующие средства чтения входных данных и форматирования, основанные на System.Text.Json, обрабатывают все остальные JSON-запросы и JSON-ответы.

Используйте метод Newtonsoft.Json.JsonConvert.SerializeObject для сериализации JsonPatchDocument.

Метод HTTP-запроса PATCH

Методы PUT и PATCH используются для обновления существующего ресурса. Различие между ними заключается в том, что PUT заменяет весь ресурс, а PATCH указывает только изменения.

Исправление JSON

Формат JSON Patch используется для указания обновлений, применяемых к ресурсу. Документ JSON Patch содержит массив операций. Каждая операция определяет определенный тип изменения. Примеры таких изменений включают добавление элемента массива или замена значения свойства.

Например, следующие документы JSON представляют ресурс, документ исправления JSON для ресурса и результат применения операций исправления.

Пример ресурса

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

Пример JSON Patch

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

В приведённом выше JSON:

  • свойство op указывает тип операции;
  • свойство path указывает обновляемый элемент;
  • свойство value предоставляет новое значение.

Ресурс после обновления

Ниже показан ресурс в том состоянии, которое он принимает после применения приведенного выше документа JSON Patch.

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

Изменения, внесенные при применении документа исправления JSON к ресурсу, являются атомарными. Если любая операция в списке завершается ошибкой, операция в списке не применяется.

Синтаксис пути

Свойство Path в объекте операции содержит косые черты между уровнями. Например, "/address/zipCode".

Для указания элементов массива используются числовые индексы, начиная с нуля. Первый элемент массива addresses будет обозначаться как /addresses/0. В add конце массива используйте дефис (-) вместо номера индекса: /addresses/-

Operations

В следующей таблице перечислены поддерживаемые операции, которые определены в спецификации JSON Patch.

Operation Notes
add Добавить свойство или элемент массива. Для существующего свойства устанавливает значение.
remove Удалите свойство или элемент массива.
replace То же, что remove, затем add в том же месте.
move То же, что remove из источника, а затем add в место назначения, используя значение из источника.
copy Действует так же, как add, в котором указаны место назначения и значение из источника.
test Возвращает код состояния успеха, если значение по адресу path равно указанному value.

Исправление JSON в ASP.NET Core

Реализация ASP.NET Core для JSON Patch предоставляется в пакете NuGet Microsoft.AspNetCore.JsonPatch.

Код метода действия

В контроллере API метод действия для JSON Patch:

  • помечен атрибутом HttpPatch;
  • принимает JsonPatchDocument<TModel> обычно с указанием [FromBody];
  • Вызывает ApplyTo(Object) для документа исправления, чтобы применить изменения.

Приведем пример:

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

Этот код из примера приложения работает со следующей 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; }
}

Этот пример метода действия:

  • Создает документ Customer.
  • Применяет исправление.
  • возвращает результат в тексте ответа.

В реальном приложении код будет извлекать данные из хранилища, например из базы данных, и обновлять эту базу данных после применения исправлений.

Состояние модели

Указанный выше пример метода действия вызывает перегрузку ApplyTo, которая принимает состояние модели в качестве одного из параметров. В этом случае вы получаете в ответах сообщения об ошибках. В следующем примере показан текст ответа с кодом 400 "Неверный запрос" для операции test:

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

Динамические объекты

В следующем примере метода действия показано, как применить исправление к динамическому объекту:

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

    return Ok(obj);
}

Операция добавления

  • Если path указывает на элемент массива, новый элемент вставляется перед тем, который указан в параметре path.
  • Если path указывает на свойство, задается значение свойства.
  • Если path указывает на несуществующее расположение:
    • Если обновляемый ресурс является динамическим объектом, добавляется свойство.
    • Если обновляемый ресурс является статическим объектом, запрос завершается ошибкой.

Следующий пример документа с исправлениями устанавливает значение CustomerName и добавляет объект Order в конец массива Orders.

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

Операция удаления

  • Если path указывает на элемент массива, элемент удаляется.
  • Если path указывает на свойство:
    • Если обновляемый ресурс является динамическим объектом, свойство удаляется.
    • Если ресурс для исправления является статическим объектом:
      • Если свойство допускает значение null: задает его равным null.
      • Если свойство не может принимать значения NULL, ему присваивается значение default<T>.

Следующий пример документа исправления устанавливает для CustomerName значение null и удаляет Orders[0]:

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

Операция замены

Эта операция функционально эквивалентна remove, за которой следует add.

Следующий пример документа исправления задает значение CustomerName и заменяет Orders[0] новым объектом Order:

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

Операция перемещения

  • Если path указывает на элемент массива, элемент from копируется в расположение элемента path, а затем выполняется операция remove для элемента from.
  • Если path указывает на свойство, значение свойства from копируется в свойство path, а затем выполняется операция remove для свойства from.
  • Если path указывает на несуществующее свойство:
    • Если обновляемый ресурс является статическим объектом, запрос завершается ошибкой.
    • Если исправляемый ресурс является динамическим объектом, значение from копируется в местоположение, указанное параметром path, а затем выполняется операция remove для свойства from.

Следующий пример документа с исправлением:

  • копирует значение Orders[0].OrderName в CustomerName;
  • присваивает Orders[0].OrderName значение NULL;
  • перемещает Orders[1] в расположение перед Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Операция копирования

Эта операция функционально аналогична операции move без последнего шага remove.

Следующий пример документа с исправлением:

  • копирует значение Orders[0].OrderName в CustomerName;
  • вставляет копию Orders[1] перед Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Операция тестирования

Если значение в расположении, которое задано параметром path, отличается от предоставленного в value значения, запрос завершится ошибкой. В этом случае весь запрос PATCH завершается с ошибкой, даже если все остальные операции в документе PATCH в противном случае были бы успешно выполнены.

Операция test традиционно используется для предотвращения обновлений при возможных конфликтах параллелизма.

Следующий пример документа исправления не оказывает никакого влияния, если исходное значение CustomerName — "John", поскольку проверка не проходит:

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

Получение кода

Просмотреть или скачать образец кода. (Инструкция по скачиванию.)

Чтобы проверить этот пример, запустите приложение и отправьте HTTP-запросы со следующими параметрами:

  • URL-адрес: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Метод HTTP: PATCH
  • заголовок: Content-Type: application/json-patch+json;
  • Текст: скопируйте и вставьте один из примеров документов JSON Patch из папки проекта JSON.

Снижение рисков безопасности

При использовании пакета Microsoft.AspNetCore.JsonPatch в реализации на основе Newtonsoft.Json, важно критически понимать и устранять потенциальные риски безопасности. В следующих разделах описаны выявленные риски безопасности, связанные с исправлением JSON, и рекомендуемые способы устранения рисков для обеспечения безопасного использования пакета.

Important

Это не исчерпывающий список угроз. Разработчики приложений должны проводить собственные проверки модели угроз, чтобы определить полный список конкретных приложений и при необходимости придумать соответствующие меры по устранению рисков. Например, приложения, которые делают коллекции доступными для операций исправления, должны учитывать потенциал для атак алгоритмической сложности, если эти операции вставляют или удаляют элементы в начале коллекции.

Выполняя комплексные модели угроз для собственных приложений и устраняя выявленные угрозы, следуя приведенным ниже рекомендациям, потребители этих пакетов могут интегрировать функции исправления JSON в свои приложения, минимизируя риски безопасности.

Отказ в обслуживании (DoS) через увеличение памяти

  • Сценарий: вредоносный клиент отправляет copy операцию, которая дублирует графы больших объектов несколько раз, что приводит к чрезмерному потреблению памяти.
  • Влияние: потенциальные условия исчерпанияOf-Memory (OOM), приводящие к сбоям в работе службы.
  • Mitigation:
    • Перед вызовом ApplyTo проверьте входящие JSON-патчи на соответствие размера и структуры.
    • Проверка должна быть конкретной для приложения, но пример проверки может выглядеть следующим образом:
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();
    }
}

Подверсия бизнес-логики

  • Сценарий. Операции исправления могут управлять полями с неявными инвариантными (например, внутренними флагами, идентификаторами или вычисляемыми полями), нарушая бизнес-ограничения.
  • Влияние: проблемы целостности данных и непреднамеренное поведение приложения.
  • Mitigation:
    • Используйте объекты POCO с явно определенными свойствами, безопасными для изменения.
    • Избегайте предоставления конфиденциальных или критически важных свойств безопасности в целевом объекте.
    • Если объект POCO не используется, проверьте исправленный объект после применения операций, чтобы убедиться, что бизнес-правила и инварианты не нарушаются.

Проверка подлинности и авторизация

  • Сценарий. Неавторизованные или несанкционированные клиенты отправляют вредоносные запросы на исправление JSON.
  • Влияние: несанкционированный доступ к изменению конфиденциальных данных или нарушению поведения приложения.
  • Mitigation:
    • Защита конечных точек, принимаюющих запросы на исправление JSON с помощью надлежащих механизмов проверки подлинности и авторизации.
    • Ограничить доступ к доверенным клиентам или пользователям с соответствующими разрешениями.

Дополнительные ресурсы

В этой статье описывается обработка запросов JSON Patch в веб-API ASP.NET Core.

Important

Стандарт JSON Patch имеет присущие риски безопасности. Поскольку эти риски присущи стандарту исправлений JSON, эта реализация не пытается снизить риски, связанные с безопасностью. Разработчик отвечает за безопасное применение документа исправления JSON к целевому объекту. Дополнительные сведения см. в разделе "Устранение рисков безопасности ".

Установка пакета

Чтобы включить поддержку исправлений JSON в приложении, выполните следующие действия.

  1. Установите пакет NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  2. Обновите метод проекта Startup.ConfigureServices для вызова AddNewtonsoftJson. Рассмотрим пример.

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson совместим с методами регистрации службы MVC:

JSON Patch, AddNewtonsoftJson и System.Text.Json

AddNewtonsoftJson заменяет средства форматирования ввода и вывода на основе System.Text.Json, используемые для форматирования всего содержимого JSON. Чтобы добавить поддержку исправления JSON с помощью Newtonsoft.Json, оставляя другие форматировщики без изменений, обновите метод проекта Startup.ConfigureServices следующим образом:

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

Для предыдущего кода требуется Microsoft.AspNetCore.Mvc.NewtonsoftJson пакет и следующие 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;

Используйте Newtonsoft.Json.JsonConvert.SerializeObject метод для сериализации JsonPatchDocument.

Метод HTTP-запроса PATCH

Методы PUT и PATCH используются для обновления существующего ресурса. Различие между ними заключается в том, что PUT заменяет весь ресурс, а PATCH указывает только изменения.

Исправление JSON

Формат JSON Patch используется для указания обновлений, применяемых к ресурсу. Документ JSON Patch содержит массив операций. Каждая операция определяет определенный тип изменения. Примеры таких изменений включают добавление элемента массива или замена значения свойства.

Например, следующие документы JSON представляют ресурс, документ исправления JSON для ресурса и результат применения операций исправления.

Пример ресурса

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

Пример JSON Patch

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

В приведённом выше JSON:

  • свойство op указывает тип операции;
  • свойство path указывает обновляемый элемент;
  • свойство value предоставляет новое значение.

Ресурс после обновления

Ниже показан ресурс в том состоянии, которое он принимает после применения приведенного выше документа JSON Patch.

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

Изменения, внесенные при применении документа исправления JSON к ресурсу, являются атомарными. Если любая операция в списке завершается ошибкой, операция в списке не применяется.

Синтаксис пути

Свойство Path в объекте операции содержит косые черты между уровнями. Например, "/address/zipCode".

Для указания элементов массива используются числовые индексы, начиная с нуля. Первый элемент массива addresses будет обозначаться как /addresses/0. В add конце массива используйте дефис (-) вместо номера индекса: /addresses/-

Operations

В следующей таблице перечислены поддерживаемые операции, которые определены в спецификации JSON Patch.

Operation Notes
add Добавить свойство или элемент массива. Для существующего свойства устанавливает значение.
remove Удалите свойство или элемент массива.
replace То же, что remove, затем add в том же месте.
move То же, что remove из источника, а затем add в место назначения, используя значение из источника.
copy Действует так же, как add, в котором указаны место назначения и значение из источника.
test Возвращает код состояния успеха, если значение по адресу path равно указанному value.

Исправление JSON в ASP.NET Core

Реализация ASP.NET Core для JSON Patch предоставляется в пакете NuGet Microsoft.AspNetCore.JsonPatch.

Код метода действия

В контроллере API метод действия для JSON Patch:

  • помечен атрибутом HttpPatch;
  • принимает JsonPatchDocument<T> обычно с указанием [FromBody];
  • Вызывает ApplyTo для документа исправления, чтобы применить изменения.

Приведем пример:

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

Этот код из примера приложения работает со следующей 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; }
    }
}

Этот пример метода действия:

  • Создает документ Customer.
  • Применяет исправление.
  • возвращает результат в тексте ответа.

В реальном приложении код будет извлекать данные из хранилища, например из базы данных, и обновлять эту базу данных после применения исправлений.

Состояние модели

Указанный выше пример метода действия вызывает перегрузку ApplyTo, которая принимает состояние модели в качестве одного из параметров. В этом случае вы получаете в ответах сообщения об ошибках. В следующем примере показан текст ответа с кодом 400 "Неверный запрос" для операции test:

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

Динамические объекты

В следующем примере метода действия показано, как применить исправление к динамическому объекту:

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

    return Ok(obj);
}

Операция добавления

  • Если path указывает на элемент массива, новый элемент вставляется перед тем, который указан в параметре path.
  • Если path указывает на свойство, задается значение свойства.
  • Если path указывает на несуществующее расположение:
    • Если обновляемый ресурс является динамическим объектом, добавляется свойство.
    • Если обновляемый ресурс является статическим объектом, запрос завершается ошибкой.

Следующий пример документа с исправлениями устанавливает значение CustomerName и добавляет объект Order в конец массива Orders.

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

Операция удаления

  • Если path указывает на элемент массива, элемент удаляется.
  • Если path указывает на свойство:
    • Если обновляемый ресурс является динамическим объектом, свойство удаляется.
    • Если ресурс для исправления является статическим объектом:
      • Если свойство допускает значение null: задает его равным null.
      • Если свойство не может принимать значения NULL, ему присваивается значение default<T>.

Следующий пример документа исправления устанавливает для CustomerName значение null и удаляет Orders[0]:

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

Операция замены

Эта операция функционально эквивалентна remove, за которой следует add.

Следующий пример документа исправления задает значение CustomerName и заменяет Orders[0] новым объектом Order:

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

Операция перемещения

  • Если path указывает на элемент массива, элемент from копируется в расположение элемента path, а затем выполняется операция remove для элемента from.
  • Если path указывает на свойство, значение свойства from копируется в свойство path, а затем выполняется операция remove для свойства from.
  • Если path указывает на несуществующее свойство:
    • Если обновляемый ресурс является статическим объектом, запрос завершается ошибкой.
    • Если исправляемый ресурс является динамическим объектом, значение from копируется в местоположение, указанное параметром path, а затем выполняется операция remove для свойства from.

Следующий пример документа с исправлением:

  • копирует значение Orders[0].OrderName в CustomerName;
  • присваивает Orders[0].OrderName значение NULL;
  • перемещает Orders[1] в расположение перед Orders[0].
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Операция копирования

Эта операция функционально аналогична операции move без последнего шага remove.

Следующий пример документа с исправлением:

  • копирует значение Orders[0].OrderName в CustomerName;
  • вставляет копию Orders[1] перед Orders[0].
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

Операция тестирования

Если значение в расположении, которое задано параметром path, отличается от предоставленного в value значения, запрос завершится ошибкой. В этом случае весь запрос PATCH завершается с ошибкой, даже если все остальные операции в документе PATCH в противном случае были бы успешно выполнены.

Операция test традиционно используется для предотвращения обновлений при возможных конфликтах параллелизма.

Следующий пример документа исправления не оказывает никакого влияния, если исходное значение CustomerName — "John", поскольку проверка не проходит:

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

Получение кода

Просмотреть или скачать образец кода. (Инструкция по скачиванию.)

Чтобы проверить этот пример, запустите приложение и отправьте HTTP-запросы со следующими параметрами:

  • URL-адрес: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • Метод HTTP: PATCH
  • заголовок: Content-Type: application/json-patch+json;
  • Текст: скопируйте и вставьте один из примеров документов JSON Patch из папки проекта JSON.