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 操作通常不使用与 POSTPUT 操作相同的模型定义,正是出于这个原因:你如何区分 null , 和 don't changeFrom 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));