【问题标题】:ASP.NET Web API 2 and partial updatesASP.NET Web API 2 和部分更新
【发布时间】:2017-12-09 05:33:05
【问题描述】:

我们正在使用 ASP.NET Web API 2,并希望以以下方式公开部分编辑某些对象的能力:

HTTP 补丁 /customers/1
{
  "firstName": "John",
  "lastName": null
}

...将firstName 设置为"John"lastName 设置为null

HTTP 补丁 /customers/1
{
  "firstName": "John"
}

...为了将firstName 更新为"John" 并且根本不要触摸lastName。假设我们有很多属性要使用这种语义进行更新。

这是一种非常方便的行为,例如 OData

问题是默认的 JSON 序列化器在这两种情况下都只会出现null,所以无法区分。

我正在寻找某种方法来使用某种包装器(内部设置/取消设置值和标志)来注释模型,以便看到这种差异。任何现有的解决方案?

【问题讨论】:

  • 为什么不使用仅具有您需要的属性的其他模型?
  • 使用Dictionary<string,object> 仅传递需要更新的属性。使用提供的 id 从存储中检索模型,然后您可以使用反射仅更新有效负载中发送的匹配属性。
  • @Nkosi,最好还是列出强类型模型和所有可能的属性。
  • @mason,这是有问题的,我同意你的观点并感谢你的想法,但我想为我提出的问题找到一个可能的解决方案,而不是确信这不是我需要的。
  • 这篇文章可能会有所帮助...stackoverflow.com/questions/14177676/…

标签: c# asp.net asp.net-web-api asp.net-web-api2 patch


【解决方案1】:

这是我的快速且廉价的解决方案...

public static ObjectType Patch<ObjectType>(ObjectType source, JObject document)
    where ObjectType : class
{
    JsonSerializerSettings settings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    };

    try
    {
        String currentEntry = JsonConvert.SerializeObject(source, settings);

        JObject currentObj = JObject.Parse(currentEntry);

        foreach (KeyValuePair<String, JToken> property in document)
        {    
            currentObj[property.Key] = property.Value;
        }

        String updatedObj = currentObj.ToString();

        return JsonConvert.DeserializeObject<ObjectType>(updatedObj);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

从基于 PATCH 的方法中获取请求正文时,请确保将参数作为类型,例如 JObject。迭代期间的 JObject 返回一个 KeyValuePair 结构,它本质上简化了修改过程。这使您可以在不接收所需类型的反序列化结果的情况下获取请求正文内容。

这是有益的,因为您不需要对无效属性进行任何额外的验证。如果您希望使您的值无效,这也有效,因为 Patch&lt;ObjectType&gt;() 方法仅循环通过部分 JSON 文档中给出的属性。

使用Patch&lt;ObjectType&gt;() 方法,您只需传递您的源或目标实例,以及将更新您的对象的部分JSON 文档。此方法将应用基于 camelCase 的合约解析器,以防止生成不兼容和不准确的属性名称。然后,此方法会将您传递的某种类型的实例序列化并转换为 JObject。

然后,该方法将新 JSON 文档中的所有属性替换为当前和序列化的文档,而无需任何不必要的 if 语句。

该方法将当前已修改的文档字符串化,并将修改后的 JSON 文档反序列化为所需的泛型类型。

如果发生异常,该方法将简单地抛出它。是的,这很不具体,但你是程序员,你需要知道会发生什么......

这一切都可以通过以下简单的语法完成:

Entity entity = AtomicModifier.Patch<Entity>(entity, partialDocument);

这是操作通常的样子:

// Partial JSON document (originates from controller).
JObject newData = new { role = 9001 };

// Current entity from EF persistence medium.
User user = await context.Users.FindAsync(id);

// Output:
//
//     Username : engineer-186f
//     Role     : 1
//
Debug.WriteLine($"Username : {0}", user.Username);
Debug.WriteLine($"Role     : {0}", user.Role);

// Partially updated entity.
user = AtomicModifier.Patch<User>(user, newData);

// Output:
//
//     Username : engineer-186f
//     Role     : 9001
//
Debug.WriteLine($"Username : {0}", user.Username);
Debug.WriteLine($"Role     : {0}", user.Role);

// Setting the new values to the context.
context.Entry(user).State = EntityState.Modified;

如果您可以使用 camelCase 合同解析器正确映射您的两个文档,则此方法将很有效。

享受...

更新

我用下面的代码更新了Patch&lt;T&gt;()方法...

public static T PatchObject<T>(T source, JObject document) where T : class
{
    Type type = typeof(T);

    IDictionary<String, Object> dict = 
        type
            .GetProperties()
            .ToDictionary(e => e.Name, e => e.GetValue(source));

    string json = document.ToString();

    var patchedObject = JsonConvert.DeserializeObject<T>(json);

    foreach (KeyValuePair<String, Object> pair in dict)
    {
        foreach (KeyValuePair<String, JToken> node in document)
        {
            string propertyName =   char.ToUpper(node.Key[0]) + 
                                    node.Key.Substring(1);

            if (propertyName == pair.Key)
            {
                PropertyInfo property = type.GetProperty(propertyName);

                property.SetValue(source, property.GetValue(patchedObject));

                break;
            }
        }
    }

    return source;
}

【讨论】:

    【解决方案2】:

    我知道已经给出的答案已经涵盖了所有方面,但我只想简要总结一下我们最终做了什么,以及什么似乎对我们很有效。

    创建了通用数据合约

    [DataContract]
    public class RQFieldPatch<T>
    {
        [DataMember(Name = "value")]
        public T Value { get; set; }
    }
    

    为补丁请求创建临时数据合同

    示例如下。

    [DataContract]
    public class PatchSomethingRequest
    {
        [DataMember(Name = "prop1")]
        public RQFieldPatch<EnumTypeHere> Prop1 { get; set; }
    
        [DataMember(Name = "prop2")]
        public RQFieldPatch<ComplexTypeContractHere> Prop2 { get; set; }
    
        [DataMember(Name = "prop3")]
        public RQFieldPatch<string> Prop3 { get; set; }
    
        [DataMember(Name = "prop4")]
        public RQFieldPatch<int> Prop4 { get; set; }
    
        [DataMember(Name = "prop5")]
        public RQFieldPatch<int?> Prop5 { get; set; }
    }
    

    业务逻辑

    简单。

    if (request.Prop1 != null)
    {
        // update code for Prop1, the value is stored in request.Prop1.Value
    }
    

    Json 格式

    简单。不像“JSON Patch”标准那么广泛,但涵盖了我们所有的需求。

    {
      "prop1": null, // will be skipped
      // "prop2": null // skipped props also skipped as they will get default (null) value
      "prop3": { "value": "test" } // value update requested
    }
    

    属性

    • 简单的合约,简单的逻辑
    • 无序列化自定义
    • 支持空值赋值
    • 涵盖任何类型:值、引用、复杂的自定义类型等等

    【讨论】:

      【解决方案3】:

      我知道我在这个答案上有点晚了,但我认为我有一个不需要更改序列化并且也不包括反射的解决方案(This article 向您推荐一个 JsonPatch 库有人写了使用反射)。

      基本上创建一个表示可以修补的属性的通用类

          public class PatchProperty<T> where T : class
          {
              public bool Include { get; set; }
              public T Value { get; set; }
          }
      

      然后创建表示您要修补的对象的模型,其中每个属性都是 PatchProperty

          public class CustomerPatchModel
          {
              public PatchProperty<string> FirstName { get; set; }
              public PatchProperty<string> LastName { get; set; }
              public PatchProperty<int> IntProperty { get; set; }
          }
      

      那么你的 WebApi 方法看起来像

          public void PatchCustomer(CustomerPatchModel customerPatchModel)
          {
              if (customerPatchModel.FirstName?.Include == true)
              {
                  // update first name 
                  string firstName = customerPatchModel.FirstName.Value;
              }
              if (customerPatchModel.LastName?.Include == true)
              {
                  // update last name
                  string lastName = customerPatchModel.LastName.Value;
              }
              if (customerPatchModel.IntProperty?.Include == true)
              {
                  // update int property
                  int intProperty = customerPatchModel.IntProperty.Value;
              }
          }
      

      你可以用一些看起来像这样的 Json 发送请求

      {
          "LastName": { "Include": true, "Value": null },
          "OtherProperty": { "Include": true, "Value": 7 }
      }
      

      然后我们会知道忽略 FirstName 但仍将其他属性分别设置为 null 和 7。

      请注意,我没有对此进行测试,也不能 100% 确定它会起作用。它基本上依赖于 .NET 序列化通用 PatchProperty 的能力。但由于模型上的属性指定了泛型 T 的类型,我认为它可以。此外,由于我们在 PatchProperty 声明中有“where T : class”,因此 Value 应该可以为空。我很想知道这是否真的有效。在最坏的情况下,您可以为所有属性类型实现 StringPatchProperty、IntPatchProperty 等。

      【讨论】:

      • 这几乎就是我最终得到的结果,但是,为什么需要那个“包含”属性?您可以只检查 PatchProperty 是否为空(不是值)。我们也可以一次性覆盖值类型。
      【解决方案4】:

      起初我误解了这个问题。当我使用 Xml 时,我认为这很容易。只需向属性添加一个属性并将属性留空。但正如我发现的那样,Json 不是那样工作的。由于我一直在寻找适用于 xml 和 json 的解决方案,因此您将在此答案中找到 xml 引用。另一件事,我写这个时考虑到了一个 C# 客户端。

      第一步是创建两个用于序列化的类。

      public class ChangeType
      {
          [JsonProperty("#text")]
          [XmlText]
          public string Text { get; set; }
      }
      
      public class GenericChangeType<T> : ChangeType
      {
      }
      

      我选择了泛型和非泛型类,因为很难转换为泛型类型,而这并不重要。此外,对于 xml 实现,XmlText 必须是字符串。

      XmlText 是属性的实际值。优点是您可以向此对象添加属性,并且这是一个对象,而不仅仅是字符串。在 XML 中它看起来像:&lt;Firstname&gt;John&lt;/Firstname&gt;

      对于 Json,这不起作用。 Json 不知道属性。所以对于 Json 这只是一个具有属性的类。为了实现 xml 值的想法(稍后我会谈到),我将属性重命名为 #text。这只是一个约定。

      由于 XmlText 是字符串(我们想序列化为字符串),因此可以存储值而忽略类型。但是在序列化的情况下,我想知道实际的类型。

      缺点是viewmodel需要引用这些类型,优点是属性为序列化强类型:

      public class CustomerViewModel
      {
          public GenericChangeType<int> Id { get; set; }
          public ChangeType Firstname { get; set; }
          public ChangeType Lastname { get; set; }
          public ChangeType Reference { get; set; }
      }
      

      假设我设置了值:

      var customerViewModel = new CustomerViewModel
      {
          // Where int needs to be saved as string.
          Id = new GenericeChangeType<int> { Text = "12" },
          Firstname = new ChangeType { Text = "John" },
          Lastname = new ChangeType { },
          Reference = null // May also be omitted.
      }
      

      在 xml 中,这看起来像:

      <CustomerViewModel>
        <Id>12</Id>
        <Firstname>John</Firstname>
        <Lastname />
      </CustomerViewModel>
      

      这足以让服务器检测到更改。但是使用 json 会生成以下内容:

      {
          "id": { "#text": "12" },
          "firstname": { "#text": "John" },
          "lastname": { "#text": null }
      }
      

      它可以工作,因为在我的实现中,接收视图模型具有相同的定义。但是由于您只是在谈论序列化,并且如果您使用其他实现,您会想要:

      {
          "id": 12,
          "firstname": "John",
          "lastname": null
      }
      

      这就是我们需要添加一个自定义 json 转换器来产生这个结果的地方。相关代码在 WriteJson 中,假设您只将此转换器添加到序列化程序设置中。但为了完整起见,我也添加了 readJson 代码。

      public class ChangeTypeConverter : JsonConverter
      {
          public override bool CanConvert(Type objectType)
          {
              // This is important, we can use this converter for ChangeType only
              return typeof(ChangeType).IsAssignableFrom(objectType);
          }
      
          public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
          {
              var value = JToken.Load(reader);
      
              // Types match, it can be deserialized without problems.
              if (value.Type == JTokenType.Object)
                  return JsonConvert.DeserializeObject(value.ToString(), objectType);
      
              // Convert to ChangeType and set the value, if not null:
              var t = (ChangeType)Activator.CreateInstance(objectType);
              if (value.Type != JTokenType.Null)
                  t.Text = value.ToString();
              return t;
          }
      
          public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
          {
              var d = value.GetType();
      
              if (typeof(ChangeType).IsAssignableFrom(d))
              {
                  var changeObject = (ChangeType)value;
      
                  // e.g. GenericChangeType<int>
                  if (value.GetType().IsGenericType)
                  {
                      try
                      {
                          // type - int
                          var type = value.GetType().GetGenericArguments()[0];
                          var c = Convert.ChangeType(changeObject.Text, type);
                          // write the int value
                          writer.WriteValue(c);
                      }
                      catch
                      {
                          // Ignore the exception, just write null.
                          writer.WriteNull();
                      }
                  }
                  else
                  {
                      // ChangeType object. Write the inner string (like xmlText value)
                      writer.WriteValue(changeObject.Text);
                  }
                  // Done writing.
                  return;
              }
              // Another object that is derived from ChangeType.
              // Do not add the current converter here because this will result in a loop.
              var s = new JsonSerializer
              {
                  NullValueHandling = serializer.NullValueHandling,
                  DefaultValueHandling = serializer.DefaultValueHandling,
                  ContractResolver = serializer.ContractResolver
              };
              JToken.FromObject(value, s).WriteTo(writer);
          }
      }
      

      起初我尝试将转换器添加到类:[JsonConverter(ChangeTypeConverter)]。但问题是转换器将始终被使用,这会创建一个引用循环(如上面代码中的注释中所述)。此外,您可能只想将此转换器用于序列化。这就是为什么我只将它添加到序列化程序中:

      var serializerSettings = new JsonSerializerSettings
      {
          NullValueHandling = NullValueHandling.Ignore,
          DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
          Converters = new List<JsonConverter> { new ChangeTypeConverter() },
          ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
      };
      var s = JsonConvert.SerializeObject(customerViewModel, serializerSettings);
      

      这将生成我正在寻找的 json,并且应该足以让服务器检测到更改。

      -- 更新--

      由于此答案侧重于序列化,因此最重要的是 lastname 是序列化字符串的一部分。然后取决于接收方如何再次将字符串反序列化为对象。

      序列化和反序列化使用不同的设置。为了再次反序列化,您可以使用:

      var deserializerSettings = new JsonSerializerSettings
      {
          //NullValueHandling = NullValueHandling.Ignore,
          DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
          Converters = new List<JsonConverter> { new Converters.NoChangeTypeConverter() },
          ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
      };
      var obj = JsonConvert.DeserializeObject<CustomerViewModel>(s, deserializerSettings);
      

      如果你使用相同的类进行反序列化,那么 Request.Lastname 应该是 ChangeType,Text = null。

      我不确定为什么从反序列化设置中删除 NullValueHandling 会导致您的情况出现问题。但是您可以通过将空对象写入值而不是 null 来克服这个问题。在转换器中,当前的 ReadJson 已经可以处理这个问题。但是在 WriteJson 中必须进行修改。而不是writer.WriteValue(changeObject.Text);,您需要类似:

      if (changeObject.Text == null)
          JToken.FromObject(new ChangeType(), s).WriteTo(writer);
      else
          writer.WriteValue(changeObject.Text);
      

      这将导致:

      {
          "id": 12,
          "firstname": "John",
          "lastname": {}
      }
      

      【讨论】:

      • 谢谢,这正是我的想法。唯一需要增强的——更好地支持数字、日期等非字符串值。
      • 此外,它似乎没有提供设置 null 并查看何时反序列化该字段的“补丁”对象的能力。这意味着如果我们有"lastname": null,我们将无法将其与省略的字段区分开来。
      • 我想知道,在哪些方面应该对非字符串值有更好的支持?对我来说,这个类仅用于序列化。我可以添加那些本来会被省略的首选类型的属性。
      • 当我专注于序列化时,我没有注意到,但你是正确的,从反序列化中删除了姓氏。如果您从反序列化器设置中删除“NullValueHandling = NullValueHandling.Ignore”,那么它应该可以工作。
      • 如果我们删除NullValueHandling = NullValueHandling.Ignore,那么序列化方面就会开始受到影响——C# null 会自动转换为 JS null,按照我们的约定,这意味着“将值设置为 null”
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-02
      • 2019-02-10
      • 2016-04-12
      • 2014-10-27
      相关资源
      最近更新 更多