.NET: Workflows - Support fidelity when converting to and from ChatMessage in declarative workflows (#3505)

* Builds locally and tests pass

* Fix typo

* Updated

* Updated

* Fixed tests failing on net472 but not on dotnet10

---------

Co-authored-by: Chris Rickman <crickman@microsoft.com>
This commit is contained in:
Peter Ibekwe
2026-01-30 17:14:24 +00:00
committed by GitHub
co-authored by Chris Rickman
parent 924211a518
commit ff7041b990
6 changed files with 187 additions and 28 deletions
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
@@ -666,4 +667,107 @@ public sealed class ChatMessageExtensionsTests
RecordValue metadataRecord = Assert.IsType<RecordValue>(metadataField, exactMatch: false);
Assert.Equal(2, metadataRecord.Fields.Count());
}
[Fact]
public void RoundTripChatMessageAsRecord()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("Test message"),
new UriContent("https://example.com/image.jpg", "image/jpeg"),
new HostedFileContent("file_123abc"),
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
])
{
MessageId = "msg-001"
};
// Act
RecordValue result = message.ToRecord();
DataValue resultValue = result.ToDataValue();
ChatMessage? messageCopy = resultValue.ToChatMessage();
// Assert
Assert.NotNull(messageCopy);
Assert.Equal(message.Role, messageCopy.Role);
Assert.Equal(message.MessageId, messageCopy.MessageId);
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
foreach (AIContent contentCopy in messageCopy.Contents)
{
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
AssertAIContentEquivalent(sourceContent, contentCopy);
}
}
[Fact]
public void RoundTripChatMessageAsTable()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("Test message"),
new UriContent("https://example.com/image.jpg", "image/jpeg"),
new HostedFileContent("file_123abc"),
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
])
{
MessageId = "msg-001"
};
IEnumerable<ChatMessage> messages = [message];
// Act
TableValue result = messages.ToTable();
TableDataValue resultValue = result.ToTable();
ChatMessage[] messagesCopy = resultValue.ToChatMessages().ToArray();
// Assert
Assert.NotNull(messagesCopy);
ChatMessage messageCopy = Assert.Single(messagesCopy);
Assert.Equal(message.Role, messageCopy.Role);
Assert.Equal(message.MessageId, messageCopy.MessageId);
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
foreach (AIContent contentCopy in messageCopy.Contents)
{
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
AssertAIContentEquivalent(sourceContent, contentCopy);
}
}
/// <summary>
/// Compares two AIContent instances for equivalence without using Assert.Equivalent,
/// which fails on .NET Framework 4.7.2 due to ReadOnlySpan.GetHashCode() not being supported.
/// </summary>
private static void AssertAIContentEquivalent(AIContent expected, AIContent actual)
{
Assert.Equal(expected.GetType(), actual.GetType());
switch (expected)
{
case TextContent expectedText:
TextContent actualText = Assert.IsType<TextContent>(actual);
Assert.Equal(expectedText.Text, actualText.Text);
break;
case UriContent expectedUri:
UriContent actualUri = Assert.IsType<UriContent>(actual);
Assert.Equal(expectedUri.Uri, actualUri.Uri);
Assert.Equal(expectedUri.MediaType, actualUri.MediaType);
break;
case HostedFileContent expectedFile:
HostedFileContent actualFile = Assert.IsType<HostedFileContent>(actual);
Assert.Equal(expectedFile.FileId, actualFile.FileId);
break;
case DataContent expectedData:
DataContent actualData = Assert.IsType<DataContent>(actual);
Assert.Equal(expectedData.MediaType, actualData.MediaType);
Assert.Equal(expectedData.Data.ToArray(), actualData.Data.ToArray());
break;
default:
Assert.Fail($"Unexpected AIContent type: {expected.GetType().Name}");
break;
}
}
}