首页 > 解决方案 > 如何在 C# 中映射支持 2 种反序列化方式的集合?

问题描述

我正在尝试调用外部 WebAPI,并且输入的形状与输出的形状相似。我想为输入和输出重用相同类型的对象。

事实上,有一个简单的方法可以让请求和响应模型像下面这样在Attachments属性中添加额外的层:

request = new { MessageText = "text", Attachments = new {Attachment = attachments }};
response = new { MessageText = "text", Attachments = attachments };

但是我很感兴趣这两个模型是否可以由单个类表示,但与反序列化相比,序列化为不同的模式。理想情况下寻找属性的一些属性,但对自定义转换器或其他任何东西开放。

细节:

WebAPI 的输入 json 必须是这样的:

    {
      "MessageText": "text",
      "Attachments": {
        "Attachment": [
          {
            "FileName": "file1"
          },
          {
            "FileName": "file2"
          }
        ]
      }
    }

但是当 WebAPI 在结果中返回相同的对象时,它会返回:

    {
      "MessageText": "text",
      "Attachments": [
        {
          "FileName": "file1"
        },
        {
          "FileName": "file2"
        }
      ]
    }

我不知道为什么 WebAPI 在输入中需要一个级别“附件”,并且不会在输出中返回......当然,我对 WebAPI 的代码没有任何控制权。

如何使用 C# 映射我的 DTO 类?他们必须通过这 3 个单元测试?

[TestClass]
public class UnitTestJsonCollectionSerialisation
{
    private void TestMethodDeSerialization(string json)
    {
        var actual = JsonConvert.DeserializeObject<Message>(json);
        Assert.AreEqual(2, actual.Attachments.Count);
        Assert.AreEqual("file1", actual.Attachments[0].FileName);
    }

    [TestMethod]
    public void TestMethodDeSerialization1()
    {
        const string JSON1 = "{\"MessageText\":\"text\",\"Attachments\":[{\"FileName\":\"file1\"},{\"FileName\":\"file2\"}]}";
        TestMethodDeSerialization(JSON1);
    }

    [TestMethod]
    public void TestMethodDeSerialization2()
    {
        const string JSON2 = "{\"MessageText\":\"text\",\"Attachments\":{\"Attachment\":[{\"FileName\":\"file1\"},{\"FileName\":\"file2\"}]}}";
        TestMethodDeSerialization(JSON2);
    }

    [TestMethod]
    public void TestMethodSerialization()
    {
        Message message = new Message();
        message.MessageText = "text";
        message.Attachments.Add(new Attachment() { FileName = "file1" });
        message.Attachments.Add(new Attachment() { FileName = "file2" });
        string actual = JsonConvert.SerializeObject(message);
        Assert.AreEqual("{\"MessageText\":\"text\",\"Attachments\":{\"Attachment\":[{\"FileName\":\"file1\"},{\"FileName\":\"file2\"}]}}", actual);
    }
}

以下 DTO 类通过了第一个单元测试,但最后一个没有通过。

public class Message
{
    public string MessageText { get; set; }
    public Attachments Attachments { get; set; } = new Attachments();
}

public class Attachment
{
    public string FileName { get; set; }
}

public class Attachments : List<Attachment> { }

我该怎么做才能让所有单元测试都变成绿色?

标签: c#jsoncollectionsjson.netdeserialization

解决方案


JsonConverter使用带有 Json.NET 属性的标准是不可能的。

您可以为此编写自己JsonConverter<Message>的,但最好同时拥有请求和响应模型,因为它们可以相互独立地更改,因此您需要编写更复杂的转换器。

MessageJsonConverter将通过您的测试的样本

public class MessageJsonConverter : JsonConverter<Message>
{
    public override Message ReadJson(JsonReader reader, System.Type objectType, [AllowNull] Message existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        var message = existingValue ?? new Message();
        while (reader.Read() && reader.TokenType != JsonToken.EndObject)
        {
            var propertyName = (string) reader.Value;
            if (propertyName.Equals(nameof(message.MessageText), StringComparison.OrdinalIgnoreCase))
            {
                reader.Read();
                message.MessageText = (string) reader.Value;
            }
            else if (propertyName.Equals(nameof(message.Attachments), StringComparison.OrdinalIgnoreCase))
            {
                reader.Read();
                if (reader.TokenType == JsonToken.StartArray)
                {
                    message.Attachments = serializer.Deserialize<Attachments>(reader);
                }
                else if (reader.TokenType == JsonToken.StartObject)
                {
                    reader.Read();
                    var subPropertyName = (string) reader.Value;
                    if (subPropertyName.Equals("Attachment", StringComparison.OrdinalIgnoreCase))
                    {
                        reader.Read(); // now JsonToken.StartArray
                        message.Attachments = serializer.Deserialize<Attachments>(reader); // now JsonToken.EndArray
                        reader.Read(); // now JsonToken.PropertyName or JsonToken.EndObject
                        if (reader.TokenType != JsonToken.EndObject)
                            throw new JsonSerializationException($"Additional properties in [{propertyName}]");
                    }
                    else
                    {
                        throw new JsonSerializationException($"Unknown property [{propertyName}.{subPropertyName}]");
                    }
                }
            }
            else
            {
                throw new JsonSerializationException($"Unknown property [{propertyName}]");
            }
        }

        return message;
    }

    public override void WriteJson(JsonWriter writer, [AllowNull] Message value, JsonSerializer serializer)
    {
        if (value is null)
        {
            writer.WriteNull();
            return;
        }

        writer.WriteStartObject();
        {
            writer.WritePropertyName(nameof(Message.MessageText));
            writer.WriteValue(value.MessageText);

            writer.WritePropertyName(nameof(Message.Attachments));
            writer.WriteStartObject();
            {
                writer.WritePropertyName("Attachment");
                serializer.Serialize(writer, value.Attachments);
            }
            writer.WriteEndObject();
        }
        writer.WriteEndObject();
    }
}

注意:这个类可以(我几乎可以肯定它确实)包含错误,因此您需要为带有空值等的极端情况编写更多测试。


推荐阅读