Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit c11b7fd

Browse files
authored
[#6771] Fix Attachment issue when it has a MemoryStream instance (#6850)
* Fix Attachment issue when it has a MemoryStream instance * Fix unit tests
1 parent 1f6ff08 commit c11b7fd

3 files changed

Lines changed: 353 additions & 0 deletions

File tree

libraries/Microsoft.Bot.Schema/Attachment.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace Microsoft.Bot.Schema
55
{
6+
using Microsoft.Bot.Schema.Converters;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Linq;
89

@@ -53,6 +54,7 @@ public Attachment(string contentType = default, string contentUrl = default, obj
5354
/// </summary>
5455
/// <value>The embedded content.</value>
5556
[JsonProperty(PropertyName = "content")]
57+
[JsonConverter(typeof(AttachmentMemoryStreamConverter))]
5658
public object Content { get; set; }
5759

5860
/// <summary>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Linq;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
using Newtonsoft.Json.Serialization;
12+
13+
namespace Microsoft.Bot.Schema.Converters
14+
{
15+
/// <summary>
16+
/// Converter which allows a MemoryStream instance to be used during JSON serialization/deserialization.
17+
/// </summary>
18+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes.
19+
internal class AttachmentMemoryStreamConverter : JsonConverter
20+
{
21+
public override bool CanConvert(Type objectType)
22+
{
23+
return typeof(MemoryStream).IsAssignableFrom(objectType);
24+
}
25+
26+
/// <returns>
27+
/// If the object is of type:<br/>
28+
/// <list type="table">
29+
/// <item>
30+
/// <b>List/Array</b>
31+
/// <list type="bullet">
32+
/// <item><i>Without MemoryStream</i>: it will return a JArray.</item>
33+
/// <item><i>With MemoryStream</i>: it will return a List.</item>
34+
/// </list>
35+
/// </item>
36+
/// <item>
37+
/// <b>Dictionary/Object</b>
38+
/// <list type="bullet">
39+
/// <item><i>Without MemoryStream</i>: it will return a JObject.</item>
40+
/// <item><i>With MemoryStream</i>: it will return a Dictionary.</item>
41+
/// </list>
42+
/// </item>
43+
/// </list>
44+
/// </returns>
45+
/// <inheritdoc/>
46+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
47+
{
48+
if (reader.TokenType == JsonToken.Null)
49+
{
50+
return JValue.CreateNull();
51+
}
52+
53+
if (reader.TokenType == JsonToken.StartArray)
54+
{
55+
var list = new List<object>();
56+
reader.Read();
57+
while (reader.TokenType != JsonToken.EndArray)
58+
{
59+
var item = ReadJson(reader, objectType, existingValue, serializer);
60+
list.Add(item);
61+
reader.Read();
62+
}
63+
64+
if (HaveStreams(list))
65+
{
66+
return list;
67+
}
68+
else
69+
{
70+
return JArray.FromObject(list);
71+
}
72+
}
73+
74+
if (reader.TokenType == JsonToken.StartObject)
75+
{
76+
var deserialized = serializer.Deserialize<JToken>(reader);
77+
78+
var isStream = deserialized.Type == JTokenType.Object && deserialized.Value<string>("$type") == nameof(MemoryStream);
79+
if (isStream)
80+
{
81+
var stream = deserialized.ToObject<SerializedMemoryStream>();
82+
return new MemoryStream(stream.Buffer.ToArray());
83+
}
84+
85+
var newReader = deserialized.CreateReader();
86+
newReader.Read();
87+
string key = null;
88+
var dict = new Dictionary<string, object>();
89+
while (newReader.Read())
90+
{
91+
if (newReader.TokenType == JsonToken.EndObject)
92+
{
93+
continue;
94+
}
95+
96+
if (newReader.TokenType == JsonToken.PropertyName)
97+
{
98+
key = newReader.Value.ToString();
99+
continue;
100+
}
101+
102+
var item = ReadJson(newReader, objectType, existingValue, serializer);
103+
dict.Add(key, item);
104+
}
105+
106+
var list = dict.Values.ToList();
107+
if (HaveStreams(list))
108+
{
109+
return dict;
110+
}
111+
else
112+
{
113+
return JObject.FromObject(dict);
114+
}
115+
}
116+
117+
return serializer.Deserialize(reader);
118+
}
119+
120+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
121+
{
122+
if (!typeof(MemoryStream).IsAssignableFrom(value.GetType()))
123+
{
124+
if (value.GetType().GetInterface(nameof(IEnumerable)) != null)
125+
{
126+
// This makes the WriteJson loops over nested values to replace all instances of MemoryStream.
127+
serializer.Converters.Add(this);
128+
}
129+
130+
JToken.FromObject(value, serializer).WriteTo(writer);
131+
serializer.Converters.Remove(this);
132+
return;
133+
}
134+
135+
var buffer = (value as MemoryStream).ToArray();
136+
var result = new SerializedMemoryStream
137+
{
138+
Type = nameof(MemoryStream),
139+
Buffer = buffer.ToList()
140+
};
141+
142+
JToken.FromObject(result).WriteTo(writer);
143+
}
144+
145+
/// <summary>
146+
/// Check if a List contains at least one MemoryStream.
147+
/// </summary>
148+
/// <param name="list">List of values that might have a MemoryStream instance.</param>
149+
/// <returns>True if there is at least one MemoryStream in the list, otherwise false.</returns>
150+
private static bool HaveStreams(List<object> list)
151+
{
152+
var result = false;
153+
foreach (var nextLevel in list)
154+
{
155+
if (nextLevel == null)
156+
{
157+
continue;
158+
}
159+
160+
if (nextLevel.GetType() == typeof(MemoryStream))
161+
{
162+
result = true;
163+
}
164+
165+
// Type generated from the ReadJson => JsonToken.StartObject.
166+
if (nextLevel.GetType() == typeof(Dictionary<string, object>))
167+
{
168+
result = HaveStreams((nextLevel as Dictionary<string, object>).Values.ToList());
169+
}
170+
171+
// Type generated from the ReadJson => JsonToken.StartArray.
172+
if (nextLevel.GetType() == typeof(List<object>))
173+
{
174+
result = HaveStreams(nextLevel as List<object>);
175+
}
176+
177+
if (result)
178+
{
179+
break;
180+
}
181+
}
182+
183+
return result;
184+
}
185+
186+
internal class SerializedMemoryStream
187+
{
188+
[JsonProperty("$type")]
189+
public string Type { get; set; }
190+
191+
[JsonProperty("buffer")]
192+
public List<byte> Buffer { get; set; }
193+
}
194+
}
195+
#pragma warning restore CA1812
196+
}

tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Text;
8+
using Newtonsoft.Json;
59
using Newtonsoft.Json.Linq;
610
using Xunit;
711

@@ -98,5 +102,156 @@ public void AttachmentViewInits()
98102
Assert.Equal(viewId, attachmentView.ViewId);
99103
Assert.Equal(size, attachmentView.Size);
100104
}
105+
106+
[Fact]
107+
public void AttachmentShouldWorkWithoutJsonConverter()
108+
{
109+
var text = "Hi!";
110+
var activity = new ActivityDummy
111+
{
112+
Attachments = new Attachment[]
113+
{
114+
new AttachmentDummy { ContentType = "string", Content = text },
115+
new AttachmentDummy { ContentType = "string/array", Content = new string[] { text } },
116+
new AttachmentDummy { ContentType = "dict", Content = new Dictionary<string, object> { { "firstname", "John" }, { "attachment1", new AttachmentDummy(content: text) }, { "lastname", "Doe" }, { "attachment2", new AttachmentDummy(content: text) }, { "age", 18 } } },
117+
new AttachmentDummy { ContentType = "attachment", Content = new AttachmentDummy(content: text) },
118+
new AttachmentDummy { ContentType = "attachment/dict", Content = new Dictionary<string, AttachmentDummy> { { "attachment", new AttachmentDummy(content: text) }, { "attachment2", new AttachmentDummy(content: text) } } },
119+
new AttachmentDummy { ContentType = "attachment/dict/nested", Content = new Dictionary<string, Dictionary<string, AttachmentDummy>> { { "attachment", new Dictionary<string, AttachmentDummy> { { "content", new AttachmentDummy(content: text) } } } } },
120+
new AttachmentDummy { ContentType = "attachment/list", Content = new List<AttachmentDummy> { new AttachmentDummy(content: text), new AttachmentDummy(content: text) } },
121+
new AttachmentDummy { ContentType = "attachment/list/nested", Content = new List<List<AttachmentDummy>> { new List<AttachmentDummy> { new AttachmentDummy(content: text) } } },
122+
}
123+
};
124+
125+
AssertAttachment(activity);
126+
}
127+
128+
[Fact]
129+
public void AttachmentShouldWorkWithJsonConverter()
130+
{
131+
var text = "Hi!";
132+
var activity = new Activity
133+
{
134+
Attachments = new Attachment[]
135+
{
136+
new Attachment { ContentType = "string", Content = text },
137+
new Attachment { ContentType = "string/array", Content = new string[] { text } },
138+
new Attachment { ContentType = "dict", Content = new Dictionary<string, object> { { "firstname", "John" }, { "attachment1", new Attachment(content: text) }, { "lastname", "Doe" }, { "attachment2", new Attachment(content: text) }, { "age", 18 } } },
139+
new Attachment { ContentType = "attachment", Content = new Attachment(content: text) },
140+
new Attachment { ContentType = "attachment/dict", Content = new Dictionary<string, Attachment> { { "attachment", new Attachment(content: text) } } },
141+
new Attachment { ContentType = "attachment/dict/nested", Content = new Dictionary<string, Dictionary<string, Attachment>> { { "attachment", new Dictionary<string, Attachment> { { "content", new Attachment(content: text) } } } } },
142+
new Attachment { ContentType = "attachment/list", Content = new List<Attachment> { new Attachment(content: text), new Attachment(content: text) } },
143+
new Attachment { ContentType = "attachment/list/nested", Content = new List<List<Attachment>> { new List<Attachment> { new Attachment(content: text) } } },
144+
}
145+
};
146+
147+
AssertAttachment(activity);
148+
}
149+
150+
[Fact]
151+
public void MemoryStreamAttachmentShouldWorkWithJsonConverter()
152+
{
153+
var text = "Hi!";
154+
var buffer = Encoding.UTF8.GetBytes(text);
155+
var activity = new Activity
156+
{
157+
Attachments = new Attachment[]
158+
{
159+
new Attachment { ContentType = "stream", Content = new MemoryStream(buffer) },
160+
new Attachment { ContentType = "stream/empty", Content = new MemoryStream() },
161+
new Attachment { ContentType = "stream/dict", Content = new Dictionary<string, MemoryStream> { { "stream", new MemoryStream(buffer) } } },
162+
new Attachment { ContentType = "stream/dict/nested", Content = new Dictionary<string, Dictionary<string, MemoryStream>> { { "stream", new Dictionary<string, MemoryStream> { { "content", new MemoryStream(buffer) } } } } },
163+
new Attachment { ContentType = "stream/list", Content = new List<MemoryStream> { new MemoryStream(buffer), new MemoryStream(buffer) } },
164+
new Attachment { ContentType = "stream/list/nested", Content = new List<List<MemoryStream>> { new List<MemoryStream> { new MemoryStream(buffer) } } },
165+
}
166+
};
167+
168+
var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null });
169+
var deserialized = JsonConvert.DeserializeObject<Activity>(serialized);
170+
171+
var buffer0 = (GetAttachmentContentByType(deserialized, "stream") as MemoryStream).ToArray();
172+
var buffer1 = (GetAttachmentContentByType(deserialized, "stream/empty") as MemoryStream).ToArray();
173+
var buffer2 = ((GetAttachmentContentByType(deserialized, "stream/dict") as Dictionary<string, object>)["stream"] as MemoryStream).ToArray();
174+
var buffer3 = (((GetAttachmentContentByType(deserialized, "stream/dict/nested") as Dictionary<string, object>)["stream"] as Dictionary<string, object>)["content"] as MemoryStream).ToArray();
175+
var buffer4 = ((GetAttachmentContentByType(deserialized, "stream/list") as List<object>)[0] as MemoryStream).ToArray();
176+
var buffer4_1 = ((GetAttachmentContentByType(deserialized, "stream/list") as List<object>)[1] as MemoryStream).ToArray();
177+
var buffer5 = (((GetAttachmentContentByType(deserialized, "stream/list/nested") as List<object>)[0] as List<object>)[0] as MemoryStream).ToArray();
178+
179+
Assert.Equal(text, Encoding.UTF8.GetString(buffer0));
180+
Assert.Equal(buffer, buffer0);
181+
Assert.Equal([], buffer1);
182+
Assert.Equal(buffer, buffer2);
183+
Assert.Equal(buffer, buffer3);
184+
Assert.Equal(buffer, buffer4);
185+
Assert.Equal(buffer, buffer4_1);
186+
Assert.Equal(buffer, buffer5);
187+
}
188+
189+
[Fact]
190+
public void MemoryStreamAttachmentShouldFailWithoutJsonConverter()
191+
{
192+
var text = "Hi!";
193+
var buffer = Encoding.UTF8.GetBytes(text);
194+
var activity = new ActivityDummy
195+
{
196+
Attachments = new Attachment[]
197+
{
198+
new AttachmentDummy { ContentType = "stream", Content = new MemoryStream(buffer) },
199+
}
200+
};
201+
202+
var ex = Assert.Throws<JsonSerializationException>(() => JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null }));
203+
Assert.Contains("ReadTimeout", ex.Message);
204+
}
205+
206+
private void AssertAttachment<T>(T activity)
207+
where T : Activity
208+
{
209+
var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null });
210+
var deserialized = JsonConvert.DeserializeObject<T>(serialized);
211+
212+
var attachment0 = GetAttachmentContentByType(deserialized, "string") as string;
213+
var attachment1 = (GetAttachmentContentByType(deserialized, "string/array") as JArray).First.Value<string>();
214+
var attachment2 = GetAttachmentContentByType(deserialized, "dict") as JObject;
215+
var attachment3 = (GetAttachmentContentByType(deserialized, "attachment") as JObject).Value<string>("content");
216+
var attachment4 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value<string>("content");
217+
var attachment5 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value<string>("content");
218+
var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[0].Value<string>("content");
219+
var attachment6_1 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[1].Value<string>("content");
220+
var attachment7 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value<string>("content");
221+
222+
var expectedString = GetAttachmentContentByType(activity, "string") as string;
223+
var expectedDict = GetAttachmentContentByType(activity, "dict") as Dictionary<string, object>;
224+
Assert.Equal(expectedString, attachment0);
225+
Assert.Equal(expectedString, attachment1);
226+
Assert.Equal($"{expectedDict["firstname"]} {expectedDict["lastname"]} {expectedDict["age"]}", $"{attachment2["firstname"]} {attachment2["lastname"]} {attachment2["age"]}");
227+
Assert.Equal(expectedString, attachment3);
228+
Assert.Equal(expectedString, attachment4);
229+
Assert.Equal(expectedString, attachment5);
230+
Assert.Equal(expectedString, attachment6);
231+
Assert.Equal(expectedString, attachment6_1);
232+
Assert.Equal(expectedString, attachment7);
233+
}
234+
235+
private object GetAttachmentContentByType<T>(T activity, string contenttype)
236+
where T : Activity
237+
{
238+
var attachment = activity.Attachments.First(e => e.ContentType == contenttype);
239+
return attachment.Content ?? (attachment as AttachmentDummy).Content;
240+
}
241+
242+
public class ActivityDummy : Activity
243+
{
244+
}
245+
246+
public class AttachmentDummy : Attachment
247+
{
248+
public AttachmentDummy(object content = default)
249+
{
250+
Content = content;
251+
}
252+
253+
[JsonProperty(PropertyName = "content")]
254+
public new object Content { get; set; }
255+
}
101256
}
102257
}

0 commit comments

Comments
 (0)