Skip to content

Commit

Permalink
[#6771] Fix Attachment issue when it has a MemoryStream instance (#6850)
Browse files Browse the repository at this point in the history
* Fix Attachment issue when it has a MemoryStream instance

* Fix unit tests
  • Loading branch information
sw-joelmut authored and Tracy Boehrer committed Sep 19, 2024
1 parent d6827f9 commit 425edca
Showing 3 changed files with 353 additions and 0 deletions.
2 changes: 2 additions & 0 deletions libraries/Microsoft.Bot.Schema/Attachment.cs
Original file line number Diff line number Diff line change
@@ -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
/// </summary>
/// <value>The embedded content.</value>
[JsonProperty(PropertyName = "content")]
[JsonConverter(typeof(AttachmentMemoryStreamConverter))]
public object Content { get; set; }

/// <summary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// 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
{
/// <summary>
/// Converter which allows a MemoryStream instance to be used during JSON serialization/deserialization.
/// </summary>
#pragma warning disable CA1812 // Avoid uninstantiated internal classes.
internal class AttachmentMemoryStreamConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(MemoryStream).IsAssignableFrom(objectType);
}

/// <returns>
/// If the object is of type:<br/>
/// <list type="table">
/// <item>
/// <b>List/Array</b>
/// <list type="bullet">
/// <item><i>Without MemoryStream</i>: it will return a JArray.</item>
/// <item><i>With MemoryStream</i>: it will return a List.</item>
/// </list>
/// </item>
/// <item>
/// <b>Dictionary/Object</b>
/// <list type="bullet">
/// <item><i>Without MemoryStream</i>: it will return a JObject.</item>
/// <item><i>With MemoryStream</i>: it will return a Dictionary.</item>
/// </list>
/// </item>
/// </list>
/// </returns>
/// <inheritdoc/>
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<object>();
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<JToken>(reader);

var isStream = deserialized.Type == JTokenType.Object && deserialized.Value<string>("$type") == nameof(MemoryStream);
if (isStream)
{
var stream = deserialized.ToObject<SerializedMemoryStream>();
return new MemoryStream(stream.Buffer.ToArray());
}

var newReader = deserialized.CreateReader();
newReader.Read();
string key = null;
var dict = new Dictionary<string, object>();
while (newReader.Read())
{
if (newReader.TokenType == JsonToken.EndObject)
{
continue;
}

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);
}

/// <summary>
/// Check if a List contains at least one MemoryStream.
/// </summary>
/// <param name="list">List of values that might have a MemoryStream instance.</param>
/// <returns>True if there is at least one MemoryStream in the list, otherwise false.</returns>
private static bool HaveStreams(List<object> 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<string, object>))
{
result = HaveStreams((nextLevel as Dictionary<string, object>).Values.ToList());
}

// Type generated from the ReadJson => JsonToken.StartArray.
if (nextLevel.GetType() == typeof(List<object>))
{
result = HaveStreams(nextLevel as List<object>);
}

if (result)
{
break;
}
}

return result;
}

internal class SerializedMemoryStream
{
[JsonProperty("$type")]
public string Type { get; set; }

[JsonProperty("buffer")]
public List<byte> Buffer { get; set; }
}
}
#pragma warning restore CA1812
}
155 changes: 155 additions & 0 deletions tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs
Original file line number Diff line number Diff line change
@@ -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,156 @@ 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 = "dict", Content = new Dictionary<string, object> { { "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<string, AttachmentDummy> { { "attachment", new AttachmentDummy(content: text) }, { "attachment2", new AttachmentDummy(content: text) } } },
new AttachmentDummy { ContentType = "attachment/dict/nested", Content = new Dictionary<string, Dictionary<string, AttachmentDummy>> { { "attachment", new Dictionary<string, AttachmentDummy> { { "content", new AttachmentDummy(content: text) } } } } },
new AttachmentDummy { ContentType = "attachment/list", Content = new List<AttachmentDummy> { new AttachmentDummy(content: text), new AttachmentDummy(content: text) } },
new AttachmentDummy { ContentType = "attachment/list/nested", Content = new List<List<AttachmentDummy>> { new List<AttachmentDummy> { 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 = "dict", Content = new Dictionary<string, object> { { "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<string, Attachment> { { "attachment", new Attachment(content: text) } } },
new Attachment { ContentType = "attachment/dict/nested", Content = new Dictionary<string, Dictionary<string, Attachment>> { { "attachment", new Dictionary<string, Attachment> { { "content", new Attachment(content: text) } } } } },
new Attachment { ContentType = "attachment/list", Content = new List<Attachment> { new Attachment(content: text), new Attachment(content: text) } },
new Attachment { ContentType = "attachment/list/nested", Content = new List<List<Attachment>> { new List<Attachment> { 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<string, MemoryStream> { { "stream", new MemoryStream(buffer) } } },
new Attachment { ContentType = "stream/dict/nested", Content = new Dictionary<string, Dictionary<string, MemoryStream>> { { "stream", new Dictionary<string, MemoryStream> { { "content", new MemoryStream(buffer) } } } } },
new Attachment { ContentType = "stream/list", Content = new List<MemoryStream> { new MemoryStream(buffer), new MemoryStream(buffer) } },
new Attachment { ContentType = "stream/list/nested", Content = new List<List<MemoryStream>> { new List<MemoryStream> { new MemoryStream(buffer) } } },
}
};

var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null });
var deserialized = JsonConvert.DeserializeObject<Activity>(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<string, object>)["stream"] as MemoryStream).ToArray();
var buffer3 = (((GetAttachmentContentByType(deserialized, "stream/dict/nested") as Dictionary<string, object>)["stream"] as Dictionary<string, object>)["content"] as MemoryStream).ToArray();
var buffer4 = ((GetAttachmentContentByType(deserialized, "stream/list") as List<object>)[0] as MemoryStream).ToArray();
var buffer4_1 = ((GetAttachmentContentByType(deserialized, "stream/list") as List<object>)[1] as MemoryStream).ToArray();
var buffer5 = (((GetAttachmentContentByType(deserialized, "stream/list/nested") as List<object>)[0] as List<object>)[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, buffer4_1);
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<JsonSerializationException>(() => JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null }));
Assert.Contains("ReadTimeout", ex.Message);
}

private void AssertAttachment<T>(T activity)
where T : Activity
{
var serialized = JsonConvert.SerializeObject(activity, new JsonSerializerSettings { MaxDepth = null });
var deserialized = JsonConvert.DeserializeObject<T>(serialized);

var attachment0 = GetAttachmentContentByType(deserialized, "string") as string;
var attachment1 = (GetAttachmentContentByType(deserialized, "string/array") as JArray).First.Value<string>();
var attachment2 = GetAttachmentContentByType(deserialized, "dict") as JObject;
var attachment3 = (GetAttachmentContentByType(deserialized, "attachment") as JObject).Value<string>("content");
var attachment4 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value<string>("content");
var attachment5 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value<string>("content");
var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[0].Value<string>("content");
var attachment6_1 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray)[1].Value<string>("content");
var attachment7 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value<string>("content");

var expectedString = GetAttachmentContentByType(activity, "string") as string;
var expectedDict = GetAttachmentContentByType(activity, "dict") as Dictionary<string, object>;
Assert.Equal(expectedString, attachment0);
Assert.Equal(expectedString, attachment1);
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>(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; }
}
}
}

0 comments on commit 425edca

Please sign in to comment.