ASP.NET Web API 2和部分更新 [英] ASP.NET Web API 2 and partial updates

查看:86
本文介绍了ASP.NET Web API 2和部分更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We are using ASP.NET Web API 2 and want to expose ability to partially edit some object in the following fashion:

HTTP PATCH /customers/1

{
  "firstName": "John",
  "lastName": null
}

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

... to set firstName to "John" and lastName to null.

HTTP PATCH /customers/1

{
  "firstName": "John"
}

...只是为了将firstName更新为"John"而完全不触摸lastName.假设我们有很多要用这种语义更新的属性.

... in order just to update firstName to "John" and do not touch lastName at all. Suppose we have a lot of properties that we want to update with such semantic.

这是 OData .

问题在于,在两种情况下,默认的JSON序列化程序都只会提供null,因此无法区分.

The problem is that default JSON serializer will just come up with null in both cases, so it's impossible to distinguish.

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

I'm looking for some way to annotate model with some kind of wrappers (with value and flag set/unset inside) that would allow to see this difference. Any existing solutions for this?

推荐答案

起初,我误解了这个问题.当我使用Xml时,我认为这很容易.只需在属性中添加一个属性,然后将该属性保留为空即可.但是我发现,杰森不是那样工作的.由于我一直在寻找一种适用于xml和json的解决方案,因此您将在此答案中找到xml引用.另一件事,我是在考虑C#客户端的情况下编写的.

At first I misunderstood the problem. As I was working with Xml I thought it was quite easy. Just add an attribute to the property and leave the property empty. But as I found out, Json doesn't work like that. Since I was looking for a solution that works for both xml and json, you'll find xml references in this answer. Another thing, I wrote this with a C# client in mind.

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

The first step is to create two classes for serialization.

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

public class GenericChangeType<T> : ChangeType
{
}

我选择了泛型和非泛型类,因为在这并不重要的情况下很难转换为泛型类型.另外,对于xml实现,XmlText必须为字符串.

I've chosen for a generic and a non-generic class because it is hard to cast to a generic type while this is not important. Also, for xml implementation it is necessary that XmlText is string.

XmlText是属性的实际值.优点是您可以向该对象添加属性,并且可以将其添加为对象,而不仅仅是字符串.在Xml中,它看起来像:<Firstname>John</Firstname>

XmlText is the actual value of the property. The advantage is that you can add attributes to this object and the fact that this is an object, not just string. In Xml it looks like: <Firstname>John</Firstname>

对于Json,这不起作用.杰森不知道属性.因此,对于Json来说,这只是一个带有属性的类.为了实现xml值的概念(我将在稍后进行介绍),我将该属性重命名为 #text .这只是一个约定.

For Json this doesn't work. Json doesn't know attributes. So for Json this is just a class with properties. To implement the idea of the xml value (I will get to that later), I've renamed the property to #text. This is just a convention.

由于XmlText是字符串(并且我们想序列化为字符串),因此可以存储不考虑类型的值.但是在序列化的情况下,我想知道实际的类型.

As XmlText is string (and we want to serialize to string), this is fine to store the value disregard the type. But in case of serialization, I want to know the actual type.

缺点是视图模型需要引用这些类型,优点是为串行化强烈地键入了属性:

The drawback is that the viewmodel needs to reference these types, the advantage is that the properties are strongly typed for serialization:

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中,它看起来像:

In xml this will look like:

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

服务器足以检测到更改.但是使用json它将生成以下内容:

Which is enough for the server to detect the changes. But with json it will generate the following:

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

它可以工作,因为在我的实现中,接收视图模型具有相同的定义.但是由于您仅在谈论序列化,并且在使用其他实现的情况下,您可能需要:

It can work, because in my implementation the receiving viewmodel has the same definition. But since you are talking about serialization only and in case you use another implementation you would want:

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

这是我们需要添加自定义json转换器以产生此结果的地方.假设您仅将此转换器添加到串行器设置中,相关代码在WriteJson中.但是为了完整起见,我还添加了readJson代码.

That is where we need to add a custom json converter to produce this result. The relevant code is in WriteJson, assuming you would add this converter to the serializer settings only. But for the sake of completeness I've added the readJson code as well.

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)].但是问题在于,转换器将始终使用,这会创建一个参考循环(也如上面代码的注释中所述).另外,您可能只想将此转换器用于序列化.这就是为什么我只将其添加到序列化器中的原因:

At first I tried to add the converter to the class: [JsonConverter(ChangeTypeConverter)]. But the problem is that the converter will be used at all times, which creates a reference loop (as also mentioned in the comment in the code above). Also you may want to use this converter for serialization only. That is why I've added it to the serializer only:

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,并且应该足以让服务器检测到更改.

This will generate the json I was looking for and should be enough to let the server detect the changes.

-更新-

因为此答案集中在序列化上,所以最重要的是姓氏是序列化字符串的一部分.然后,取决于接收方如何再次将字符串反序列化为对象.

As this answer focusses on serialization, the most important thing is that lastname is part of the serialization string. It then depends on the receiving party how to deserialize the string into an object again.

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

Serialization and deserialization use different settings. In order to deserialize again you can use:

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.

If you use the same classes for deserialization then Request.Lastname should be of ChangeType, with Text = null.

我不确定为什么从反序列化设置中删除NullValueHandling会导致您遇到的问题.但是您可以通过将一个空对象作为值而不是null来克服此问题.在转换器中,当前的ReadJson已经可以解决这个问题.但是在WriteJson中必须进行修改.代替writer.WriteValue(changeObject.Text);,您需要像这样的东西:

I'm not sure why removing the NullValueHandling from the deserialization settings causes problems in your case. But you can overcome this by writing an empty object as value instead of null. In the converter the current ReadJson can already handle this. But in WriteJson there has to be a modification. Instead of writer.WriteValue(changeObject.Text); you need something like:

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

这将导致:

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

这篇关于ASP.NET Web API 2和部分更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆