From 006a5d9a9b3eb4ed41f63beb6341bc44e724d9db Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 1 May 2026 16:31:14 +0800 Subject: [PATCH] .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`) 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 Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: SergeyMenshykh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Functions/KernelFunctionLogMessages.cs | 17 +++++++- .../KernelFunctionLogMessagesTests.cs | 40 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs index 42c4b7f6e6..83374fe870 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunctionLogMessages.cs @@ -161,7 +161,22 @@ internal static partial class KernelFunctionLogMessages } catch (NotSupportedException ex) { - s_logFunctionResultValue(logger, pluginName, functionName, "Failed to log function result value", ex); + // Fall back to ToString() when JSON serialization isn't supported for this type + // (e.g. Microsoft.Extensions.AI.TextContent is not registered in AbstractionsJsonContext) + try + { + var toStringValue = resultValue?.Value?.ToString() ?? string.Empty; + s_logFunctionResultValue(logger, pluginName, functionName, toStringValue, null); + } + catch (Exception toStringEx) + { + s_logFunctionResultValue( + logger, + pluginName, + functionName, + "Failed to log function result value", + new AggregateException(ex, toStringEx)); + } } } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs index ec2642fa12..24daeccb63 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelFunctionLogMessagesTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -10,7 +11,7 @@ using Xunit; namespace SemanticKernel.UnitTests.Functions; -public class KernelFunctionLogMessagesTests +public partial class KernelFunctionLogMessagesTests { [Theory] [InlineData(typeof(string))] @@ -48,9 +49,46 @@ public class KernelFunctionLogMessagesTests It.IsAny>())); } + [Fact] + public void ItShouldFallBackToToStringWhenJsonSerializationIsNotSupported() + { + // Arrange + var logger = new Mock(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + // TypeNotInJsonContext cannot be cast to string and is not registered in the restricted JSON context + var unserializableValue = new TypeNotInJsonContext(); + var functionResult = new FunctionResult(KernelFunctionFactory.CreateFromMethod(() => { }), unserializableValue); + + // Use a restricted JsonSerializerOptions that knows about object but not TypeNotInJsonContext, + // simulating the AOT scenario where AbstractionsJsonContext is used and an unregistered + // MEAI type (e.g. Microsoft.Extensions.AI.TextContent) is returned from an MCP tool. + var restrictedOptions = RestrictedJsonContext.Default.Options; + + // Act + logger.Object.LogFunctionResultValue("p1", "f1", functionResult, restrictedOptions); + + // Assert - ToString() fallback should have been used, not the error message + logger.Verify(l => l.Log( + LogLevel.Trace, + 0, + It.Is((o, _) => o.ToString() == "Function p1-f1 result: TypeNotInJsonContext()"), + null, + It.IsAny>())); + } + private sealed class User { [JsonPropertyName("name")] public string? Name { get; set; } } + + private sealed class TypeNotInJsonContext + { + public override string ToString() => "TypeNotInJsonContext()"; + } + + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(object))] + private sealed partial class RestrictedJsonContext : JsonSerializerContext { } }