Web API 2 - 实施补丁
Web API 2 - Implementing a PATCH
我目前有一个实现 RESTFul API 的 Web 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
}
如果我将它作为我的正文作为 PUT 请求发送到 http://localhost:8311/api/v1/Member/12
,我的数据中 ID 为 12 的记录将更新为您在 JSON.[=23= 中看到的内容]
我想做的是实现一个 PATCH 动词,我可以在其中进行部分更新。如果 Sara 结婚,我希望能够发送这个 JSON:
{
lastName: "Jones"
}
我希望能够仅发送 JSON 并仅更新 LastName
字段并保留所有其他字段。
我试过这个:
[Route("{id}")]
[HttpPatch]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id,
[FromBody]Models.Member model)
{
}
我的问题是 returns model
对象中的所有字段(除 LastName
字段外,所有字段都是空值),这是有道理的,因为我是说我想要一个 Models.Member
对象。我想知道的是,是否有一种方法可以检测 JSON 请求中实际发送了哪些属性,以便我可以只更新那些字段?
PATCH
操作通常不使用与 POST
或 PUT
操作相同的模型定义,正是出于这个原因:你如何区分 null
, 和 don't change
。 From the IETF:
With PATCH, however, the enclosed entity contains a set of
instructions describing how a resource currently residing on the
origin server should be modified to produce a new version.
您可以在 here 中寻找他们的 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" }
]
@Tipx 关于使用 PATCH
的回答是正确的,但正如您可能已经发现的那样,实际上用 C# 这样的静态类型语言实现这一点是一项非常重要的练习。
在您使用 PATCH
表示单个域实体的一组部分更新的情况下(例如,仅为具有更多属性的联系人更新名字和姓氏)您需要按照循环 'PATCH' 请求中的每条指令然后将该指令应用于 class.
的实例的方式执行某些操作
应用单个指令将包括
- 正在查找与名称匹配的实例的 属性
指令,或处理 属性 您未预料到的名称
- 对于更新:尝试将补丁中提交的值解析到实例中 属性 并处理错误,例如实例 属性 是一个 bool 但补丁指令包含一个日期
- 决定如何处理 Add 指令,因为您无法将新属性添加到静态类型的 C# class。一种方法是说 Add 表示 "set the value of the instance's property only if property's existing value is null"
对于完整的 .NET Framework 上的 Web API 2,JSONPatch github project 看起来试图提供此代码,尽管看起来并没有太多开发repo 最近和自述文件确实声明:
This is still very much an early project, don't use it in production
yet unless you understand the source and don't mind fixing a few bugs
;)
.NET Core 上的事情更简单,因为它在 Microsoft.AspNetCore.JsonPatch
namespace.
中具有一组支持此功能的功能
相当有用的 jsonpatch.com 网站还列出了 .NET 补丁的更多选项:
- Asp.Net Core JsonPatch (Microsoft official implementation)
- Ramone (a framework for consuming REST services, includes a JSON Patch implementation)
- JsonPatch (Adds JSON Patch support to ASP.NET Web API)
- Starcounter (In-memory Application Engine, uses JSON Patch with OT for client-server sync)
- Nancy.JsonPatch (Adds JSON Patch support to NancyFX)
- Manatee.Json (JSON-everything, including JSON Patch)
我需要将此功能添加到我们现有的 Web API 2 项目中,因此如果我在这样做时发现任何其他有用的内容,我将更新此答案。
我希望这对使用 Microsoft JsonPatchDocument 有所帮助:
.Net Core 2.1 Patch Action into a Controller:
[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);
}
}
节点型号class:
[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; }
}
仅更新 "full_name" 和 "node_id" 属性的有效补丁 JSon 将是一组操作,例如:
[
{ "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" }
]
这是我基于C#中的Patch("replace")规范使用反射构建的一个扩展方法,你可以使用它来序列化任何对象来执行Patch("replace")操作,你也可以传递所需的编码,它将 return 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 也使用我创建的这个 class 使用 DataContractJsonSerializer 序列化 PatchObject:
[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; }
}
如何使用扩展方法和使用 HttpClient 调用补丁请求的 C# 示例:
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;
谢谢
我想实现完全相同的效果,但使用了与此处描述的其他方法不同的方法。如果你想查看它,我已经使用它创建了一个工作仓库:
https://github.com/emab/patch-example
如果您有以下两种型号:
数据库模型
public class WeatherDBModel
{
[Key]
public int Id { get; set; }
public string City { get; set; }
public string Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
public Weather(int id, string city, string country, double temperature, double windSpeed, double rain)
{
Id = id;
City = city;
Country = country;
Temperature = temperature;
WindSpeed = windSpeed;
Rain = rain;
}
}
更新模型
包含数据库模型属性的确切名称。包括可以更新的属性
public class WeatherUpdateModel
{
public string? City { get; set; }
public string? Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
}
此更新模型与您要更新的对象的 id
一起发送到服务层。
然后,您可以在存储库层中实施以下方法,该方法将 updateModel
中的任何非空值映射到现有实体(如果已找到):
public Weather Update(int id, WeatherUpdate updateObject)
{
// find existing entity
var existingEntity = _context.Weather.Find(id);
// handle not found
if (existingEntity == null)
{
throw new EntityNotFoundException(id);
}
// iterate through all of the properties of the update object
// in this example it includes all properties apart from `id`
foreach (PropertyInfo prop in updateObject.GetType().GetProperties())
{
// check if the property has been set in the updateObject
// if it is null we ignore it. If you want to allow null values to be set, you could add a flag to the update object to allow specific nulls
if (prop.GetValue(updateObject) != null)
{
// if it has been set update the existing entity value
existingEntity.GetType().GetProperty(prop.Name)?.SetValue(existingEntity, prop.GetValue(updateObject));
}
}
_context.SaveChanges();
return existingEntity;
}
使用此方法,您可以更改模型而无需担心更新逻辑,只要您确保 UpdateModel
与数据库模型保持同步即可。
如果在 JSON 中省略了对象的 属性,ASP.NET 将不会“设置”对象上的 属性,属性 将具有其默认值。为了知道 JSON 对象发送了哪些属性,您需要有一种方法来检测设置了对象的哪些属性。
为了检测 JSON 对象“实际发送”了哪些属性,您可以修改 Member
class 以包含 属性 的集合“设置”的名称。然后,对于您希望能够知道它们是否在 JSON 对象中发送的所有属性,当设置 属性 时,应将 属性 的名称添加到集合属性。
public class Member
{
private string _firstName;
private string _lastName;
...
private bool _isDeleted;
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
_setProperties.Add(nameof(FirstName));
}
}
public string LastName
{
get => _lastName;
set
{
_lastName = value;
_setProperties.Add(nameof(LastName));
}
}
...
public bool IsDeleted
{
get => _isDeleted;
set
{
_isDeleted= value;
_setProperties.Add(nameof(IsDeleted));
}
}
private readonly HashSet<string> _setProperties = new HashSet<string>();
public HashSet<string> GetTheSetProperties()
{
return new HashSet<string>(_setProperties);
}
}
在 UpdateRow
方法中,您现在可以检查 属性 是否在 JSON 中发送,方法是检查它是否在 _setProperties
集合中。因此,如果您想查看 LastName
是否在 JSON 中发送,只需执行
bool lastNameWasInJson = model.Contains(nameof(model.LastName));
我目前有一个实现 RESTFul API 的 Web 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
}
如果我将它作为我的正文作为 PUT 请求发送到 http://localhost:8311/api/v1/Member/12
,我的数据中 ID 为 12 的记录将更新为您在 JSON.[=23= 中看到的内容]
我想做的是实现一个 PATCH 动词,我可以在其中进行部分更新。如果 Sara 结婚,我希望能够发送这个 JSON:
{
lastName: "Jones"
}
我希望能够仅发送 JSON 并仅更新 LastName
字段并保留所有其他字段。
我试过这个:
[Route("{id}")]
[HttpPatch]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id,
[FromBody]Models.Member model)
{
}
我的问题是 returns model
对象中的所有字段(除 LastName
字段外,所有字段都是空值),这是有道理的,因为我是说我想要一个 Models.Member
对象。我想知道的是,是否有一种方法可以检测 JSON 请求中实际发送了哪些属性,以便我可以只更新那些字段?
PATCH
操作通常不使用与 POST
或 PUT
操作相同的模型定义,正是出于这个原因:你如何区分 null
, 和 don't change
。 From the IETF:
With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version.
您可以在 here 中寻找他们的 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" }
]
@Tipx 关于使用 PATCH
的回答是正确的,但正如您可能已经发现的那样,实际上用 C# 这样的静态类型语言实现这一点是一项非常重要的练习。
在您使用 PATCH
表示单个域实体的一组部分更新的情况下(例如,仅为具有更多属性的联系人更新名字和姓氏)您需要按照循环 'PATCH' 请求中的每条指令然后将该指令应用于 class.
应用单个指令将包括
- 正在查找与名称匹配的实例的 属性 指令,或处理 属性 您未预料到的名称
- 对于更新:尝试将补丁中提交的值解析到实例中 属性 并处理错误,例如实例 属性 是一个 bool 但补丁指令包含一个日期
- 决定如何处理 Add 指令,因为您无法将新属性添加到静态类型的 C# class。一种方法是说 Add 表示 "set the value of the instance's property only if property's existing value is null"
对于完整的 .NET Framework 上的 Web API 2,JSONPatch github project 看起来试图提供此代码,尽管看起来并没有太多开发repo 最近和自述文件确实声明:
This is still very much an early project, don't use it in production yet unless you understand the source and don't mind fixing a few bugs ;)
.NET Core 上的事情更简单,因为它在 Microsoft.AspNetCore.JsonPatch
namespace.
相当有用的 jsonpatch.com 网站还列出了 .NET 补丁的更多选项:
- Asp.Net Core JsonPatch (Microsoft official implementation)
- Ramone (a framework for consuming REST services, includes a JSON Patch implementation)
- JsonPatch (Adds JSON Patch support to ASP.NET Web API)
- Starcounter (In-memory Application Engine, uses JSON Patch with OT for client-server sync)
- Nancy.JsonPatch (Adds JSON Patch support to NancyFX)
- Manatee.Json (JSON-everything, including JSON Patch)
我需要将此功能添加到我们现有的 Web API 2 项目中,因此如果我在这样做时发现任何其他有用的内容,我将更新此答案。
我希望这对使用 Microsoft JsonPatchDocument 有所帮助:
.Net Core 2.1 Patch Action into a Controller:
[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);
}
}
节点型号class:
[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; }
}
仅更新 "full_name" 和 "node_id" 属性的有效补丁 JSon 将是一组操作,例如:
[
{ "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" }
]
这是我基于C#中的Patch("replace")规范使用反射构建的一个扩展方法,你可以使用它来序列化任何对象来执行Patch("replace")操作,你也可以传递所需的编码,它将 return 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 也使用我创建的这个 class 使用 DataContractJsonSerializer 序列化 PatchObject:
[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; }
}
如何使用扩展方法和使用 HttpClient 调用补丁请求的 C# 示例:
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;
谢谢
我想实现完全相同的效果,但使用了与此处描述的其他方法不同的方法。如果你想查看它,我已经使用它创建了一个工作仓库:
https://github.com/emab/patch-example
如果您有以下两种型号:
数据库模型
public class WeatherDBModel
{
[Key]
public int Id { get; set; }
public string City { get; set; }
public string Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
public Weather(int id, string city, string country, double temperature, double windSpeed, double rain)
{
Id = id;
City = city;
Country = country;
Temperature = temperature;
WindSpeed = windSpeed;
Rain = rain;
}
}
更新模型
包含数据库模型属性的确切名称。包括可以更新的属性
public class WeatherUpdateModel
{
public string? City { get; set; }
public string? Country { get; set; }
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public double Rain { get; set; }
}
此更新模型与您要更新的对象的 id
一起发送到服务层。
然后,您可以在存储库层中实施以下方法,该方法将 updateModel
中的任何非空值映射到现有实体(如果已找到):
public Weather Update(int id, WeatherUpdate updateObject)
{
// find existing entity
var existingEntity = _context.Weather.Find(id);
// handle not found
if (existingEntity == null)
{
throw new EntityNotFoundException(id);
}
// iterate through all of the properties of the update object
// in this example it includes all properties apart from `id`
foreach (PropertyInfo prop in updateObject.GetType().GetProperties())
{
// check if the property has been set in the updateObject
// if it is null we ignore it. If you want to allow null values to be set, you could add a flag to the update object to allow specific nulls
if (prop.GetValue(updateObject) != null)
{
// if it has been set update the existing entity value
existingEntity.GetType().GetProperty(prop.Name)?.SetValue(existingEntity, prop.GetValue(updateObject));
}
}
_context.SaveChanges();
return existingEntity;
}
使用此方法,您可以更改模型而无需担心更新逻辑,只要您确保 UpdateModel
与数据库模型保持同步即可。
如果在 JSON 中省略了对象的 属性,ASP.NET 将不会“设置”对象上的 属性,属性 将具有其默认值。为了知道 JSON 对象发送了哪些属性,您需要有一种方法来检测设置了对象的哪些属性。
为了检测 JSON 对象“实际发送”了哪些属性,您可以修改 Member
class 以包含 属性 的集合“设置”的名称。然后,对于您希望能够知道它们是否在 JSON 对象中发送的所有属性,当设置 属性 时,应将 属性 的名称添加到集合属性。
public class Member
{
private string _firstName;
private string _lastName;
...
private bool _isDeleted;
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
_setProperties.Add(nameof(FirstName));
}
}
public string LastName
{
get => _lastName;
set
{
_lastName = value;
_setProperties.Add(nameof(LastName));
}
}
...
public bool IsDeleted
{
get => _isDeleted;
set
{
_isDeleted= value;
_setProperties.Add(nameof(IsDeleted));
}
}
private readonly HashSet<string> _setProperties = new HashSet<string>();
public HashSet<string> GetTheSetProperties()
{
return new HashSet<string>(_setProperties);
}
}
在 UpdateRow
方法中,您现在可以检查 属性 是否在 JSON 中发送,方法是检查它是否在 _setProperties
集合中。因此,如果您想查看 LastName
是否在 JSON 中发送,只需执行
bool lastNameWasInJson = model.Contains(nameof(model.LastName));