Skip to content

Commit 006a5d9

Browse files
octo-patchocto-patchSergeyMenshykhCopilotCopilot
authored
.Net: fix: fall back to ToString() when logging function results with unregistered types (#13884)
Fixes #13681 ## Problem When a `KernelFunction` returns a value whose runtime type is not registered in `AbstractionsJsonContext` (e.g. `Microsoft.Extensions.AI.TextContent` returned by an MCP tool via `.AsKernelFunction()`), the function result logging in AOT/source-generation mode throws `NotSupportedException` during JSON serialization. The exception is caught but produces an unhelpful log entry: ``` Function SomePlugin-SomeTool result: Failed to log function result value System.NotSupportedException: JsonTypeInfo metadata for type 'Microsoft.Extensions.AI.TextContent' was not provided by TypeInfoResolver of type 'Microsoft.SemanticKernel.AbstractionsJsonContext'. ``` ## Solution Add a `ToString()` fallback in the `NotSupportedException` catch block of `LogFunctionResultValueInternal`. When JSON serialization fails because the runtime type is absent from the source-generated context, the logger now calls `resultValue?.Value?.ToString()` to produce useful output instead of the generic error message. ## Testing - Added `ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported` unit test that uses a restricted source-generated `JsonSerializerContext` (containing only `object` and `IDictionary<string, object?>`) to simulate the AOT scenario, verifying that `ToString()` output appears in the log instead of the failure message. - All existing `ItShouldLogFunctionResultOfAnyType` test cases are unchanged. --------- Co-authored-by: octo-patch <octo-patch@github.com> Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6e1ab9d commit 006a5d9

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,22 @@ private static void LogFunctionResultValueInternal(this ILogger logger, string?
161161
}
162162
catch (NotSupportedException ex)
163163
{
164-
s_logFunctionResultValue(logger, pluginName, functionName, "Failed to log function result value", ex);
164+
// Fall back to ToString() when JSON serialization isn't supported for this type
165+
// (e.g. Microsoft.Extensions.AI.TextContent is not registered in AbstractionsJsonContext)
166+
try
167+
{
168+
var toStringValue = resultValue?.Value?.ToString() ?? string.Empty;
169+
s_logFunctionResultValue(logger, pluginName, functionName, toStringValue, null);
170+
}
171+
catch (Exception toStringEx)
172+
{
173+
s_logFunctionResultValue(
174+
logger,
175+
pluginName,
176+
functionName,
177+
"Failed to log function result value",
178+
new AggregateException(ex, toStringEx));
179+
}
165180
}
166181
}
167182
}

dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Text.Json.Serialization;
56
using Microsoft.Extensions.Logging;
67
using Microsoft.SemanticKernel;
@@ -10,7 +11,7 @@
1011

1112
namespace SemanticKernel.UnitTests.Functions;
1213

13-
public class KernelFunctionLogMessagesTests
14+
public partial class KernelFunctionLogMessagesTests
1415
{
1516
[Theory]
1617
[InlineData(typeof(string))]
@@ -48,9 +49,46 @@ public void ItShouldLogFunctionResultOfAnyType(Type resultType)
4849
It.IsAny<Func<It.IsAnyType, Exception?, string>>()));
4950
}
5051

52+
[Fact]
53+
public void ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported()
54+
{
55+
// Arrange
56+
var logger = new Mock<ILogger>();
57+
logger.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);
58+
59+
// TypeNotInJsonContext cannot be cast to string and is not registered in the restricted JSON context
60+
var unserializableValue = new TypeNotInJsonContext();
61+
var functionResult = new FunctionResult(KernelFunctionFactory.CreateFromMethod(() => { }), unserializableValue);
62+
63+
// Use a restricted JsonSerializerOptions that knows about object but not TypeNotInJsonContext,
64+
// simulating the AOT scenario where AbstractionsJsonContext is used and an unregistered
65+
// MEAI type (e.g. Microsoft.Extensions.AI.TextContent) is returned from an MCP tool.
66+
var restrictedOptions = RestrictedJsonContext.Default.Options;
67+
68+
// Act
69+
logger.Object.LogFunctionResultValue("p1", "f1", functionResult, restrictedOptions);
70+
71+
// Assert - ToString() fallback should have been used, not the error message
72+
logger.Verify(l => l.Log(
73+
LogLevel.Trace,
74+
0,
75+
It.Is<It.IsAnyType>((o, _) => o.ToString() == "Function p1-f1 result: TypeNotInJsonContext()"),
76+
null,
77+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()));
78+
}
79+
5180
private sealed class User
5281
{
5382
[JsonPropertyName("name")]
5483
public string? Name { get; set; }
5584
}
85+
86+
private sealed class TypeNotInJsonContext
87+
{
88+
public override string ToString() => "TypeNotInJsonContext()";
89+
}
90+
91+
[JsonSerializable(typeof(IDictionary<string, object?>))]
92+
[JsonSerializable(typeof(object))]
93+
private sealed partial class RestrictedJsonContext : JsonSerializerContext { }
5694
}

0 commit comments

Comments
 (0)