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, поэтому я обновлю этот ответ, если найду что-нибудь еще полезное при этом.