Web API 2 - 实现PATCH

时间:2018-05-07 13:46:36

标签: c# json asp.net-mvc rest asp.net-web-api2

我目前有一个实现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
}

如果我将此身份作为我的身体发送到http://localhost:8311/api/v1/Member/12作为PUT请求,ID为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字段外都是空字符),这是有道理的,因为我说我想要{{1}对象。我想知道的是,是否有办法检测JSON请求中实际发送了哪些属性,以便我只更新这些字段?

4 个答案:

答案 0 :(得分:9)

PATCH操作通常不会使用与POSTPUT操作完全相同的模型来定义:您如何区分nulldon't changeFrom the IETF

  

但是,使用PATCH,随附的实体包含一组   说明资源当前如何驻留在   应修改原始服务器以生成新版本。

您可以查看herePATCH建议,但是sumarilly是:

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

答案 1 :(得分:1)

@Tipx的使用PATCH的答案是正确的,但是您可能已经发现,用像C#这样的静态类型的语言来实现它实际上不是一件容易的事。

如果您使用PATCH来表示单个域实体的一组部分更新(例如,仅为具有更多属性的联系人更新名和姓),则需要按照循环“ PATCH”请求中的每条指令,然后将该指令应用于类的实例的方式进行操作。

应用单个指令将包含

  • 查找与实例名称匹配的实例的属性 指令或处理您不期望的属性名称
  • 进行更新:尝试将补丁中提交的值解析为instance属性并处理错误(例如, instance属性是布尔值,但patch指令包含日期
  • 确定如何处理添加指令,因为您无法向静态类型的C#类添加新属性。一种方法是说Add意味着“仅当属性的现有值为null时才设置实例的属性的值”

对于完整.NET Framework上的Web API 2,JSONPatch github project似乎在提供此代码方面起了阻碍作用,尽管最近看起来该回购协议上没有很多开发,并且自述文件确实状态:

  

这仍然是一个早期项目,请勿在生产中使用   除非您了解来源并且不介意修复一些错误   ;)

.NET Core上的事情比较简单,因为它在Microsoft.AspNetCore.JsonPatch namespace中具有支持此功能的一组功能。

相当有用的jsonpatch.com网站还列出了.NET中Patch的更多选项:

  
      
  • Asp.Net Core JsonPatch(Microsoft官方实施)
  •   
  • Ramone(使用REST服务的框架,包括JSON Patch实现)
  •   
  • JsonPatch(将JSON补丁支持添加到ASP.NET Web API)
  •   
  • Starcounter(内存应用程序引擎,使用OT的JSON修补程序进行客户端-服务器同步)
  •   
  • Nancy.JsonPatch(向NancyFX添加了JSON补丁支持)
  •   
  • Manatee.Json(所有JSON,包括JSON补丁)
  •   

我需要将此功能添加到我们现有的Web API 2项目中,因此,如果在执行此操作时发现其他有用的内容,我将更新此答案。

答案 2 :(得分:1)

我希望这有助于使用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仅更新“ 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" }
]

这是我基于C#中的Patch(“替换”)规范构建的扩展方法,使用反射可用于序列化任何对象以执行Patch(“替换”)操作,还可以传递所需的Encoding它将返回准备发送到httpClient.PatchAsync(endPoint,httpContent)的HttpContent(StringContent):

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也使用我创建的此类使用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调用Patch请求的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;

您可以在GitHub上查看我的两个示例:

https://github.com/ernestleyva/SampleRestfulWebApi

https://github.com/ernestleyva/HttpClientApiService

谢谢

答案 3 :(得分:0)

我想实现完全相同的目标,但是使用了与此处所述其他方法不同的方法。

首先,我创建了一个UpdateModel,其中包含可以更新的所有字段,并使所有字段都可以为空。

public class UpdateModel
{
    public double? Temperature { get; set; }

    public int? Stock { get; set; }

    public string Name { get; set; }
}

此更新模型与您要更新的对象的id一起发送到服务层。

然后,服务层将此映射到database model并发送到存储库层。在存储库层更新方法中,我们要找到一个现有模型,然后将更新模型中的所有非空字段应用于该模型。我用反射来做到这一点:

public async Task<DbModel> Update(DbModel updateModel)
{
    // Find the existing model from the database
    var objToUpdate = await _context.Objects.FindAsync(updateModel.Id);
    if (mineToUpdate == null)
    {
        return null;
    }
    // Loop through all properties of the update model
    foreach (PropertyInfo prop in mine.GetType().GetProperties())
    {
        // If they are not null, update the property on the database model
        if (prop.GetValue(mine) != null)
        {
            mineToUpdate
                .GetType()
                .GetProperty(prop.Name)
                .SetValue(mineToUpdate, prop.GetValue(mine));
        }
    }
    await _context.SaveChangesAsync();
    return mineToUpdate;
}

使用此方法,只要您从更新DTO到数据库模型的映射正确,就可以更改模型而不必担心更新逻辑。