Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#6771] Fix Attachment issue when it has a MemoryStream instance #6850

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libraries/Microsoft.Bot.Schema/Attachment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Microsoft.Bot.Schema
{
using Microsoft.Bot.Schema.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <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.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);
}

/// <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
}
146 changes: 146 additions & 0 deletions tests/Microsoft.Bot.Schema.Tests/AttachmentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<string, AttachmentDummy> { { "attachment", 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 { 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 = "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 { 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 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 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, 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, "attachment") as JObject).Value<string>("content");
var attachment3 = (GetAttachmentContentByType(deserialized, "attachment/dict") as JObject).GetValue("attachment").Value<string>("content");
var attachment4 = ((GetAttachmentContentByType(deserialized, "attachment/dict/nested") as JObject).GetValue("attachment") as JObject).GetValue("content").Value<string>("content");
var attachment5 = (GetAttachmentContentByType(deserialized, "attachment/list") as JArray).First.Value<string>("content");
var attachment6 = (GetAttachmentContentByType(deserialized, "attachment/list/nested") as JArray).First.First.Value<string>("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>(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; }
}
}
}
Loading