【问题标题】:How to map Collections in C# that support 2 ways of Deserialisation?如何在 C# 中映射支持 2 种反序列化方式的集合?
【发布时间】:2020-12-04 20:26:37
【问题描述】:

我正在尝试调用外部 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# json collections json.net deserialization


    【解决方案1】:

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

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

    样本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();
        }
    }
    

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

    【讨论】:

      【解决方案2】:

      从技术上讲,这并不能回答您的确切问题,但我会使用其他方法来转换请求和响应。

      将请求和响应的序列化规则修改为不同的工作量更大,对我来说违反了principal of least astonishment。这样,任何查看Message 类代码的工程师都会立即了解发生了什么。

      class Message
      {
          public string MessageText { get; set; }
          public Attachment[] Attachments { get; set; }
      
          public string ToRequest()
          {
              return JsonConvert.SerializeObject( new { MessageText = this.MessageText, Attachments = new { Attachment = this.Attachments }});
          }
      
          static public Message FromResponse(string input)
          {
              return JsonConvert.DeserializeObject<Message>(input);
          }
      }
      

      反序列化:

      var actual = Message.FromResponse(json);
      

      序列化:

      var json = actual.ToRequest();
      

      【讨论】:

        猜你喜欢
        • 2014-12-29
        • 1970-01-01
        • 2021-09-18
        • 2019-11-20
        • 1970-01-01
        • 1970-01-01
        • 2021-03-26
        • 1970-01-01
        • 2014-08-16
        相关资源
        最近更新 更多