Web API 2 - Внедрение PATCH

В настоящее время у меня есть веб-API, который реализует RESTFul API. Модель для моего API выглядит следующим образом:

public class Member
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Created { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsDeleted { get; set; }
}

Я реализовал метод PUT для обновления строки, подобной этой (для краткости я пропустил некоторые несущественные вещи):

[Route("{id}")]
[HttpPut]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id, 
    [FromBody]Models.Member model)
{
    // Do some error checking
    // ...
    // ...

    var myDatabaseEntity = new BusinessLayer.Member(id);
    myDatabaseEntity.FirstName = model.FirstName;
    myDatabaseEntity.LastName = model.LastName;
    myDatabaseEntity.Created = model.Created;
    myDatabaseEntity.BirthDate = model.BirthDate;
    myDatabaseEntity.IsDeleted = model.IsDeleted;

    await myDatabaseEntity.SaveAsync();
}

Используя PostMan, я могу отправить следующий JSON, и все отлично работает:

{
    firstName: "Sara",
    lastName: "Smith",
    created: '2018/05/10",
    birthDate: '1977/09/12",
    isDeleted: false
}

Если я отправлю это как тело в http://localhost:8311/api/v1/Member/12 как запрос PUT, запись в моих данных с идентификатором 12 будет обновлена до того, что вы видите в JSON.

Однако я хотел бы реализовать глагол PATCH, в котором я могу выполнять частичные обновления. Если Сара выйдет замуж, я хотел бы отправить этот JSON:

{
    lastName: "Jones"
}

Я хотел бы иметь возможность отправлять только этот JSON и обновлять просто поле LastName и оставлять все остальные поля в покое.

Я попробовал это:

[Route("{id}")]
[HttpPatch]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id, 
    [FromBody]Models.Member model)
{
}

Моя проблема в том, что при этом возвращаются все поля в объекте model (все они равны нулю, кроме поля LastName), что имеет смысл, поскольку я говорю, что хочу объект Models.Member. Я хотел бы знать, есть ли способ определить, какие свойства действительно были отправлены в запросе JSON, чтобы я мог обновить только эти поля?

Ответ 1

Операции PATCH обычно не определяются с использованием той же модели, что и операции POST или PUT именно по этой причине: как вы различаете null, а a don't change. Из IETF:

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

Вы можете посмотреть здесь свое предложение PATCH, но сумарилли:

[
    { "op": "test", "path": "/a/b/c", "value": "foo" },
    { "op": "remove", "path": "/a/b/c" },
    { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
    { "op": "replace", "path": "/a/b/c", "value": 42 },
    { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
    { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

Ответ 2

Надеюсь, это поможет при использовании Microsoft JsonPatchDocument:

Действие исправления .Net Core 2.1 в контроллер:

[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody]JsonPatchDocument<Node> value)
{
    try
    {
        //nodes collection is an in memory list of nodes for this example
        var result = nodes.FirstOrDefault(n => n.Id == id);
        if (result == null)
        {
            return BadRequest();
        }    
        value.ApplyTo(result, ModelState);//result gets the values from the patch request
        return NoContent();
    }
    catch (Exception ex)
    {
        return StatusCode(StatusCodes.Status500InternalServerError, ex);
    }
}

Класс модели узла:

[DataContract(Name ="Node")]
public class Node
{
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [DataMember(Name = "node_id")]
    public int Node_id { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

    [DataMember(Name = "full_name")]
    public string Full_name { get; set; }
}

Допустимый JSon Patch для обновления только свойств "full_name" и "node_id" будет представлять собой массив операций, таких как:

[
  { "op": "replace", "path": "full_name", "value": "NewNameWithPatch"},
  { "op": "replace", "path": "node_id", "value": 10}
]

Как вы можете видеть, "op" - это операция, которую вы хотели бы выполнить, наиболее распространенной является "replace", которая просто устанавливает существующее значение этого свойства на новое, но есть и другие:

[
  { "op": "test", "path": "property_name", "value": "value" },
  { "op": "remove", "path": "property_name" },
  { "op": "add", "path": "property_name", "value": [ "value1", "value2" ] },
  { "op": "replace", "path": "property_name", "value": 12 },
  { "op": "move", "from": "property_name", "path": "other_property_name" },
  { "op": "copy", "from": "property_name", "path": "other_property_name" }
]

Вот метод расширений, который я построил на основе спецификации Patch ("заменить") в С#, используя отражение, которое вы можете использовать для сериализации любого объекта для выполнения операции Patch ("замены"), вы также можете передать желаемую кодировку и он вернет HttpContent (StringContent), готовый для отправки на httpClient.PatchAsync(endPoint, httpContent):

public static StringContent ToPatchJsonContent(this object node, Encoding enc = null)
{
    List<PatchObject> patchObjectsCollection = new List<PatchObject>();

    foreach (var prop in node.GetType().GetProperties())
    {
        var patch = new PatchObject{ Op = "replace", Path = prop.Name , Value = prop.GetValue(node) };
        patchObjectsCollection.Add(patch);                
    }

    MemoryStream payloadStream = new MemoryStream();
    DataContractJsonSerializer serializer = new DataContractJsonSerializer(patchObjectsCollection.GetType());
    serializer.WriteObject(payloadStream, patchObjectsCollection);
    Encoding encoding = enc ?? Encoding.UTF8;
    var content = new StringContent(Encoding.UTF8.GetString(payloadStream.ToArray()), encoding, "application/json");

    return content;
}

}

Заметил, что tt также использует этот класс, который я создал, для сериализации объекта PatchObject с использованием DataContractJsonSerializer:

[DataContract(Name = "PatchObject")]
class PatchObject
{
    [DataMember(Name = "op")]
    public string Op { get; set; }
    [DataMember(Name = "path")]
    public string Path { get; set; }
    [DataMember(Name = "value")]
    public object Value { get; set; }
}

С# пример использования метода расширения и вызова запроса Patch с помощью HttpClient:

    var nodeToPatch = new { Name = "TestPatch", Private = true };//You can use anonymous type
    HttpContent content = nodeToPatch.ToPatchJsonContent();//Invoke the extension method to serialize the object

    HttpClient httpClient = new HttpClient();
    string endPoint = "https://localhost:44320/api/nodes/1";
    var response = httpClient.PatchAsync(endPoint, content).Result;

Спасибо

Ответ 3

Ответ @Tipx на использование PATCH очевиден, но, как вы, вероятно, уже нашли, на самом деле добиться этого на языке статической типизации, таком как С#, нетривиальное упражнение.

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

Применение индивидуальной инструкции будет состоять из

  • Нахождение свойства экземпляра, соответствующего имени в инструкция или обработка имен свойств, которых вы не ожидали
  • Для обновления: попытка проанализировать значение, представленное в патче, в свойстве экземпляра и обработать ошибку, например, если свойство экземпляра является логическим, но инструкция исправления содержит дату
  • Решите, что делать с инструкциями Add, поскольку вы не можете добавлять новые свойства в статически типизированный класс С#. Один из подходов состоит в том, чтобы сказать, что Add означает "устанавливать значение свойства экземпляра, только если существующее значение свойства равно нулю"

Для веб-API 2 в полной версии .NET Framework проект github JSONPatch выглядит как попытка предоставить этот код, хотя, похоже, в последнее время в этом репо не было много разработок, а readme состояние:

Это все еще очень ранний проект, не используйте его в производстве пока, если вы не понимаете источник и не возражаете исправить несколько ошибок ;)

В .NET Core все проще, поскольку в нем есть набор функций для поддержки этого в пространстве имен Microsoft.AspNetCore.JsonPatch.

Довольно полезный сайт jsonpatch.com также перечисляет еще несколько вариантов исправлений в .NET:

  • Asp.Net Core JsonPatch (официальная реализация Microsoft)
  • Ramone (среда для использования служб REST, включает реализацию JSON Patch)
  • JsonPatch (добавляет поддержку JSON Patch в ASP.NET Web API)
  • Starcounter (In-memory Application Engine, использует JSON Patch с OT для синхронизации клиент-сервер)
  • Nancy.JsonPatch (добавляет поддержку JSON Patch в NancyFX)
  • Manatee.Json (JSON-все, включая JSON Patch)

Мне нужно добавить эту функциональность в наш существующий проект Web API 2, поэтому я обновлю этот ответ, если найду что-нибудь еще полезное при этом.