ASP.NET Web API 2和部分更新

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

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

…将firstName设置为"John" ,将lastNamenull

  HTTP PATCH / customers / 1 
 { "firstName": "John" } 

…只是为了将firstName更新为"John"并且根本不触及lastName 。 假设我们有很多属性要用这种语义更新。

这是OData行使的非常方便的行为。

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

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

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

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

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

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

XmlText是属性的实际值。 优点是您可以向此对象添加属性以及这是一个对象,而不仅仅是字符串。 在Xml中,它看起来像: John

对于Json,这不起作用。 Json不知道属性。 所以对于Json来说,这只是一个具有属性的类。 为了实现xml值的想法(稍后我将讨论),我将属性重命名为#text 。 这只是一个惯例。

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

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

 public class CustomerViewModel { public GenericChangeType 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 { Text = "12" }, Firstname = new ChangeType { Text = "John" }, Lastname = new ChangeType { }, Reference = null // May also be omitted. } 

在xml中,这将看起来像:

  12 John   

这足以让服务器检测到更改。 但是使用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; // eg GenericChangeType 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 { 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 { new Converters.NoChangeTypeConverter() }, ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }; var obj = JsonConvert.DeserializeObject(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": {} }