From 9e551425127395fdf05317a40ec0485f34c97fd5 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Tue, 17 Sep 2024 11:38:22 -0300 Subject: [PATCH 1/2] Fix Attachment issue when it has a MemoryStream instance --- libraries/Microsoft.Bot.Schema/Attachment.cs | 2 + .../AttachmentMemoryStreamConverter.cs | 198 ++++++++++++++++++ .../AttachmentTests.cs | 146 +++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs diff --git a/libraries/Microsoft.Bot.Schema/Attachment.cs b/libraries/Microsoft.Bot.Schema/Attachment.cs index a4291ba006..b89d102cfa 100644 --- a/libraries/Microsoft.Bot.Schema/Attachment.cs +++ b/libraries/Microsoft.Bot.Schema/Attachment.cs @@ -3,6 +3,7 @@ namespace Microsoft.Bot.Schema { + using Microsoft.Bot.Schema.Converters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -53,6 +54,7 @@ public Attachment(string contentType = default, string contentUrl = default, obj /// /// The embedded content. [JsonProperty(PropertyName = "content")] + [JsonConverter(typeof(AttachmentMemoryStreamConverter))] public object Content { get; set; } /// diff --git a/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs b/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs new file mode 100644 index 0000000000..0084926f6c --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.Bot.Schema.Converters +{ + /// + /// Converter which allows a MemoryStream instance to be used during JSON serialization/deserialization. + /// +#pragma warning disable CA1812 // Avoid uninstantiated internal classes. + internal class AttachmentMemoryStreamConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(MemoryStream).IsAssignableFrom(objectType); + } + + /// + /// If the object is of type:
+ /// + /// + /// List/Array + /// + /// Without MemoryStream: it will return a JArray. + /// With MemoryStream: it will return a List. + /// + /// + /// + /// Dictionary/Object + /// + /// Without MemoryStream: it will return a JObject. + /// With MemoryStream: it will return a Dictionary. + /// + /// + /// + ///
+ /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return JValue.CreateNull(); + } + + if (reader.TokenType == JsonToken.StartArray) + { + var list = new List(); + reader.Read(); + while (reader.TokenType != JsonToken.EndArray) + { + var item = ReadJson(reader, objectType, existingValue, serializer); + list.Add(item); + reader.Read(); + } + + if (HaveStreams(list)) + { + return list; + } + else + { + return JArray.FromObject(list); + } + } + + if (reader.TokenType == JsonToken.StartObject) + { + var deserialized = serializer.Deserialize(reader); + + var isStream = deserialized.Type == JTokenType.Object && deserialized.Value("$type") == nameof(MemoryStream); + if (isStream) + { + var stream = deserialized.ToObject(); + return new MemoryStream(stream.Buffer.ToArray()); + } + + var newReader = deserialized.CreateReader(); + newReader.Read(); + string key = null; + var dict = new Dictionary(); + while (newReader.TokenType != JsonToken.EndObject) + { + newReader.Read(); + + if (newReader.TokenType == JsonToken.EndObject) + { + break; + } + + if (newReader.TokenType == JsonToken.PropertyName) + { + key = newReader.Value.ToString(); + continue; + } + + var item = ReadJson(newReader, objectType, existingValue, serializer); + dict.Add(key, item); + } + + var list = dict.Values.ToList(); + if (HaveStreams(list)) + { + return dict; + } + else + { + return JObject.FromObject(dict); + } + } + + return serializer.Deserialize(reader); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (!typeof(MemoryStream).IsAssignableFrom(value.GetType())) + { + if (value.GetType().GetInterface(nameof(IEnumerable)) != null) + { + // This makes the WriteJson loops over nested values to replace all instances of MemoryStream. + serializer.Converters.Add(this); + } + + JToken.FromObject(value, serializer).WriteTo(writer); + serializer.Converters.Remove(this); + return; + } + + var buffer = (value as MemoryStream).ToArray(); + var result = new SerializedMemoryStream + { + Type = nameof(MemoryStream), + Buffer = buffer.ToList() + }; + + JToken.FromObject(result).WriteTo(writer); + } + + /// + /// Check if a List contains at least one MemoryStream. + /// + /// List of values that might have a MemoryStream instance. + /// True if there is at least one MemoryStream in the list, otherwise false. + private static bool HaveStreams(List list) + { + var result = false; + foreach (var nextLevel in list) + { + if (nextLevel == null) + { + continue; + } + + if (nextLevel.GetType() == typeof(MemoryStream)) + { + result = true; + } + + // Type generated from the ReadJson => JsonToken.StartObject. + if (nextLevel.GetType() == typeof(Dictionary)) + { + result = HaveStreams((nextLevel as Dictionary).Values.ToList()); + } + + // Type generated from the ReadJson => JsonToken.StartArray. + if (nextLevel.GetType() == typeof(List)) + { + result = HaveStreams(nextLevel as List); + } + + if (result) + { + break; + } + } + + return result; + } + + internal class SerializedMemoryStream + { + [JsonProperty("$type")] + public string Type { get; set; } + + [JsonProperty("buffer")] + public List Buffer { get; set; } + } + } +#pragma warning restore CA1812 +} diff --git a/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs b/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs index 783ca6cd8f..33c08734af 100644 --- a/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs +++ b/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -98,5 +102,147 @@ public void AttachmentViewInits() Assert.Equal(viewId, attachmentView.ViewId); Assert.Equal(size, attachmentView.Size); } + + [Fact] + public void AttachmentShouldWorkWithoutJsonConverter() + { + var text = "Hi!"; + var activity = new ActivityDummy + { + Attachments = new Attachment[] + { + new AttachmentDummy { ContentType = "string", Content = text }, + new AttachmentDummy { ContentType = "string/array", Content = new string[] { text } }, + new AttachmentDummy { ContentType = "attachment", Content = new AttachmentDummy(content: text) }, + new AttachmentDummy { ContentType = "attachment/dict", Content = new Dictionary { { "attachment", new AttachmentDummy(content: text) } } }, + new AttachmentDummy { ContentType = "attachment/dict/nested", Content = new Dictionary> { { "attachment", new Dictionary { { "content", new AttachmentDummy(content: text) } } } } }, + new AttachmentDummy { ContentType = "attachment/list", Content = new List { new AttachmentDummy(content: text) } }, + new AttachmentDummy { ContentType = "attachment/list/nested", Content = new List> { new List { new AttachmentDummy(content: text) } } }, + } + }; + + AssertAttachment(activity); + } + + [Fact] + public void AttachmentShouldWorkWithJsonConverter() + { + var text = "Hi!"; + var activity = new Activity + { + Attachments = new Attachment[] + { + new Attachment { ContentType = "string", Content = text }, + new Attachment { ContentType = "string/array", Content = new string[] { text } }, + new Attachment { ContentType = "attachment", Content = new Attachment(content: text) }, + new Attachment { ContentType = "attachment/dict", Content = new Dictionary { { "attachment", new Attachment(content: text) } } }, + new Attachment { ContentType = "attachment/dict/nested", Content = new Dictionary> { { "attachment", new Dictionary { { "content", new Attachment(content: text) } } } } }, + new Attachment { ContentType = "attachment/list", Content = new List { new Attachment(content: text) } }, + new Attachment { ContentType = "attachment/list/nested", Content = new List> { new List { new Attachment(content: text) } } }, + } + }; + + AssertAttachment(activity); + } + + [Fact] + public void MemoryStreamAttachmentShouldWorkWithJsonConverter() + { + var text = "Hi!"; + var buffer = Encoding.UTF8.GetBytes(text); + var activity = new Activity + { + Attachments = new Attachment[] + { + new Attachment { ContentType = "stream", Content = new MemoryStream(buffer) }, + new Attachment { ContentType = "stream/empty", Content = new MemoryStream() }, + new Attachment { ContentType = "stream/dict", Content = new Dictionary { { "stream", new MemoryStream(buffer) } } }, + new Attachment { ContentType = "stream/dict/nested", Content = new Dictionary> { { "stream", new Dictionary { { "content", new MemoryStream(buffer) } } } } }, + new Attachment { ContentType = "stream/list", Content = new List { new MemoryStream(buffer) } }, + new Attachment { ContentType = "stream/list/nested", Content = new List> { new List { new MemoryStream(buffer) } } }, + } + }; + + var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null }); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var buffer0 = (GetAttachmentContentByType(deserialized, "stream") as MemoryStream).ToArray(); + var buffer1 = (GetAttachmentContentByType(deserialized, "stream/empty") as MemoryStream).ToArray(); + var buffer2 = ((GetAttachmentContentByType(deserialized, "stream/dict") as Dictionary)["stream"] as MemoryStream).ToArray(); + var buffer3 = (((GetAttachmentContentByType(deserialized, "stream/dict/nested") as Dictionary)["stream"] as Dictionary)["content"] as MemoryStream).ToArray(); + var buffer4 = ((GetAttachmentContentByType(deserialized, "stream/list") as List)[0] as MemoryStream).ToArray(); + var buffer5 = (((GetAttachmentContentByType(deserialized, "stream/list/nested") as List)[0] as List)[0] as MemoryStream).ToArray(); + + Assert.Equal(text, Encoding.UTF8.GetString(buffer0)); + Assert.Equal(buffer, buffer0); + Assert.Equal([], buffer1); + Assert.Equal(buffer, buffer2); + Assert.Equal(buffer, buffer3); + Assert.Equal(buffer, buffer4); + Assert.Equal(buffer, buffer5); + } + + [Fact] + public void MemoryStreamAttachmentShouldFailWithoutJsonConverter() + { + var text = "Hi!"; + var buffer = Encoding.UTF8.GetBytes(text); + var activity = new ActivityDummy + { + Attachments = new Attachment[] + { + new AttachmentDummy { ContentType = "stream", Content = new MemoryStream(buffer) }, + } + }; + + var ex = Assert.Throws(() => JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null })); + Assert.Contains("ReadTimeout", ex.Message); + } + + private void AssertAttachment(T activity) + where T : Activity + { + var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null }); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var attachment0 = GetAttachmentContentByType(deserialized, "string") as string; + var attachment1 = (GetAttachmentContentByType(deserialized, "string/array") as JArray).First.Value(); + var attachment2 = (GetAttachmentContentByType(deserialized, "attachment") as JObject).Value("content"); + var attachment3 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value("content"); + var attachment4 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value("content"); + var attachment5 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray).First.Value("content"); + var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value("content"); + + var expectedString = GetAttachmentContentByType(activity, "string") as string; + Assert.Equal(expectedString, attachment0); + Assert.Equal(expectedString, attachment1); + Assert.Equal(expectedString, attachment2); + Assert.Equal(expectedString, attachment3); + Assert.Equal(expectedString, attachment4); + Assert.Equal(expectedString, attachment5); + Assert.Equal(expectedString, attachment6); + } + + private object GetAttachmentContentByType(T activity, string contenttype) + where T : Activity + { + var attachment = activity.Attachments.First(e => e.ContentType == contenttype); + return attachment.Content ?? (attachment as AttachmentDummy).Content; + } + + public class ActivityDummy : Activity + { + } + + public class AttachmentDummy : Attachment + { + public AttachmentDummy(object content = default) + { + Content = content; + } + + [JsonProperty(PropertyName = "content")] + public new object Content { get; set; } + } } } From ba77f7235d7f3463e1b7c832579ae0a952b33127 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Tue, 17 Sep 2024 16:44:29 -0300 Subject: [PATCH 2/2] Fix unit tests --- .../AttachmentMemoryStreamConverter.cs | 6 ++-- .../AttachmentTests.cs | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs b/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs index 0084926f6c..8667bd215f 100644 --- a/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs +++ b/libraries/Microsoft.Bot.Schema/Converters/AttachmentMemoryStreamConverter.cs @@ -86,13 +86,11 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist newReader.Read(); string key = null; var dict = new Dictionary(); - while (newReader.TokenType != JsonToken.EndObject) + while (newReader.Read()) { - newReader.Read(); - if (newReader.TokenType == JsonToken.EndObject) { - break; + continue; } if (newReader.TokenType == JsonToken.PropertyName) diff --git a/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs b/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs index 33c08734af..bd5b51a64f 100644 --- a/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs +++ b/tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs @@ -113,10 +113,11 @@ public void AttachmentShouldWorkWithoutJsonConverter() { new AttachmentDummy { ContentType = "string", Content = text }, new AttachmentDummy { ContentType = "string/array", Content = new string[] { text } }, + new AttachmentDummy { ContentType = "dict", Content = new Dictionary { { "firstname", "John" }, { "attachment1", new AttachmentDummy(content: text) }, { "lastname", "Doe" }, { "attachment2", new AttachmentDummy(content: text) }, { "age", 18 } } }, new AttachmentDummy { ContentType = "attachment", Content = new AttachmentDummy(content: text) }, - new AttachmentDummy { ContentType = "attachment/dict", Content = new Dictionary { { "attachment", new AttachmentDummy(content: text) } } }, + new AttachmentDummy { ContentType = "attachment/dict", Content = new Dictionary { { "attachment", new AttachmentDummy(content: text) }, { "attachment2", new AttachmentDummy(content: text) } } }, new AttachmentDummy { ContentType = "attachment/dict/nested", Content = new Dictionary> { { "attachment", new Dictionary { { "content", new AttachmentDummy(content: text) } } } } }, - new AttachmentDummy { ContentType = "attachment/list", Content = new List { new AttachmentDummy(content: text) } }, + new AttachmentDummy { ContentType = "attachment/list", Content = new List { new AttachmentDummy(content: text), new AttachmentDummy(content: text) } }, new AttachmentDummy { ContentType = "attachment/list/nested", Content = new List> { new List { new AttachmentDummy(content: text) } } }, } }; @@ -134,10 +135,11 @@ public void AttachmentShouldWorkWithJsonConverter() { new Attachment { ContentType = "string", Content = text }, new Attachment { ContentType = "string/array", Content = new string[] { text } }, + new Attachment { ContentType = "dict", Content = new Dictionary { { "firstname", "John" }, { "attachment1", new Attachment(content: text) }, { "lastname", "Doe" }, { "attachment2", new Attachment(content: text) }, { "age", 18 } } }, new Attachment { ContentType = "attachment", Content = new Attachment(content: text) }, new Attachment { ContentType = "attachment/dict", Content = new Dictionary { { "attachment", new Attachment(content: text) } } }, new Attachment { ContentType = "attachment/dict/nested", Content = new Dictionary> { { "attachment", new Dictionary { { "content", new Attachment(content: text) } } } } }, - new Attachment { ContentType = "attachment/list", Content = new List { new Attachment(content: text) } }, + new Attachment { ContentType = "attachment/list", Content = new List { new Attachment(content: text), new Attachment(content: text) } }, new Attachment { ContentType = "attachment/list/nested", Content = new List> { new List { new Attachment(content: text) } } }, } }; @@ -158,7 +160,7 @@ public void MemoryStreamAttachmentShouldWorkWithJsonConverter() new Attachment { ContentType = "stream/empty", Content = new MemoryStream() }, new Attachment { ContentType = "stream/dict", Content = new Dictionary { { "stream", new MemoryStream(buffer) } } }, new Attachment { ContentType = "stream/dict/nested", Content = new Dictionary> { { "stream", new Dictionary { { "content", new MemoryStream(buffer) } } } } }, - new Attachment { ContentType = "stream/list", Content = new List { new MemoryStream(buffer) } }, + new Attachment { ContentType = "stream/list", Content = new List { new MemoryStream(buffer), new MemoryStream(buffer) } }, new Attachment { ContentType = "stream/list/nested", Content = new List> { new List { new MemoryStream(buffer) } } }, } }; @@ -171,6 +173,7 @@ public void MemoryStreamAttachmentShouldWorkWithJsonConverter() var buffer2 = ((GetAttachmentContentByType(deserialized, "stream/dict") as Dictionary)["stream"] as MemoryStream).ToArray(); var buffer3 = (((GetAttachmentContentByType(deserialized, "stream/dict/nested") as Dictionary)["stream"] as Dictionary)["content"] as MemoryStream).ToArray(); var buffer4 = ((GetAttachmentContentByType(deserialized, "stream/list") as List)[0] as MemoryStream).ToArray(); + var buffer4_1 = ((GetAttachmentContentByType(deserialized, "stream/list") as List)[1] as MemoryStream).ToArray(); var buffer5 = (((GetAttachmentContentByType(deserialized, "stream/list/nested") as List)[0] as List)[0] as MemoryStream).ToArray(); Assert.Equal(text, Encoding.UTF8.GetString(buffer0)); @@ -179,6 +182,7 @@ public void MemoryStreamAttachmentShouldWorkWithJsonConverter() Assert.Equal(buffer, buffer2); Assert.Equal(buffer, buffer3); Assert.Equal(buffer, buffer4); + Assert.Equal(buffer, buffer4_1); Assert.Equal(buffer, buffer5); } @@ -207,20 +211,25 @@ private void AssertAttachment(T activity) var attachment0 = GetAttachmentContentByType(deserialized, "string") as string; var attachment1 = (GetAttachmentContentByType(deserialized, "string/array") as JArray).First.Value(); - var attachment2 = (GetAttachmentContentByType(deserialized, "attachment") as JObject).Value("content"); - var attachment3 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value("content"); - var attachment4 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value("content"); - var attachment5 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray).First.Value("content"); - var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value("content"); + var attachment2 = GetAttachmentContentByType(deserialized, "dict") as JObject; + var attachment3 = (GetAttachmentContentByType(deserialized, "attachment") as JObject).Value("content"); + var attachment4 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value("content"); + var attachment5 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value("content"); + var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[0].Value("content"); + var attachment6_1 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[1].Value("content"); + var attachment7 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value("content"); var expectedString = GetAttachmentContentByType(activity, "string") as string; + var expectedDict = GetAttachmentContentByType(activity, "dict") as Dictionary; Assert.Equal(expectedString, attachment0); Assert.Equal(expectedString, attachment1); - Assert.Equal(expectedString, attachment2); + Assert.Equal($"{expectedDict["firstname"]} {expectedDict["lastname"]} {expectedDict["age"]}", $"{attachment2["firstname"]} {attachment2["lastname"]} {attachment2["age"]}"); Assert.Equal(expectedString, attachment3); Assert.Equal(expectedString, attachment4); Assert.Equal(expectedString, attachment5); Assert.Equal(expectedString, attachment6); + Assert.Equal(expectedString, attachment6_1); + Assert.Equal(expectedString, attachment7); } private object GetAttachmentContentByType(T activity, string contenttype)