From e10b5714a2cf5e17803d2d3999373a07f42c3a55 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 6 May 2025 13:45:05 +0300 Subject: [PATCH 1/3] Fix AIFunctionFactory handling of default struct arguments --- .../Utilities/AIJsonUtilities.Schema.cs | 14 ++++++-------- .../Functions/AIFunctionFactoryTest.cs | 10 ++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index f0764d6fae8..1e64eae9b53 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -446,21 +446,19 @@ private static JsonElement ParseJsonElement(ReadOnlySpan utf8Json) return JsonElement.ParseValue(ref reader); } + [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", + Justification = "Called conditionally on structs whose default ctor never gets trimmed.")] private static object? GetDefaultValueNormalized(ParameterInfo parameterInfo) { // Taken from https://github.com/dotnet/runtime/blob/eff415bfd667125c1565680615a6f19152645fbf/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317 Type parameterType = parameterInfo.ParameterType; object? defaultValue = parameterInfo.DefaultValue; - if (defaultValue is null) + if (defaultValue is null || (defaultValue == DBNull.Value && parameterType != typeof(DBNull))) { - return null; - } - - // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. - if (defaultValue == DBNull.Value && parameterType != typeof(DBNull)) - { - return null; + return parameterType.IsValueType + ? Activator.CreateInstance(parameterType) + : null; } // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 0670a06b206..021eec98c1b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -809,6 +809,15 @@ public async Task MarshalResult_TypeIsDeclaredTypeEvenWhenDerivedTypeReturned() Assert.Equal("marshalResultInvoked", result); } + [Fact] + public async Task AIFunctionFactory_DefaultDefaultParameter() + { + AIFunction f = AIFunctionFactory.Create((Guid g = default) => g, serializerOptions: JsonContext.Default.Options); + + object? result = await f.InvokeAsync(); + Assert.Contains("00000000-0000-0000-0000-000000000000", result?.ToString()); + } + private sealed class MyService(int value) { public int Value => value; @@ -873,5 +882,6 @@ private sealed class C : B; [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(Guid))] private partial class JsonContext : JsonSerializerContext; } From 36ea6d7176c97fb429741362b5019ffd22c9aeae Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 6 May 2025 13:51:03 +0300 Subject: [PATCH 2/3] Extend testing to custom structs --- .../Functions/AIFunctionFactoryTest.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 021eec98c1b..4b5ff9a0600 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -812,10 +812,12 @@ public async Task MarshalResult_TypeIsDeclaredTypeEvenWhenDerivedTypeReturned() [Fact] public async Task AIFunctionFactory_DefaultDefaultParameter() { - AIFunction f = AIFunctionFactory.Create((Guid g = default) => g, serializerOptions: JsonContext.Default.Options); + Assert.NotEqual(new StructWithDefaultCtor().Value, default(StructWithDefaultCtor).Value); + + AIFunction f = AIFunctionFactory.Create((Guid g = default, StructWithDefaultCtor s = default) => g.ToString() + "," + s.Value.ToString(), serializerOptions: JsonContext.Default.Options); object? result = await f.InvokeAsync(); - Assert.Contains("00000000-0000-0000-0000-000000000000", result?.ToString()); + Assert.Contains("00000000-0000-0000-0000-000000000000,0", result?.ToString()); } private sealed class MyService(int value) @@ -880,8 +882,19 @@ private class A; private class B : A; private sealed class C : B; + public readonly struct StructWithDefaultCtor + { + public int Value { get; } + public StructWithDefaultCtor() + { + Value = 42; + } + } + [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(string))] [JsonSerializable(typeof(Guid))] + [JsonSerializable(typeof(StructWithDefaultCtor))] private partial class JsonContext : JsonSerializerContext; } From 9bfcfb5ff62fe429d7874d617ced70fb99f52f92 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 6 May 2025 15:48:48 +0300 Subject: [PATCH 3/3] Use GetUninitializedObject instead of Activator --- .../Utilities/AIJsonUtilities.Schema.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index 1e64eae9b53..a0afa66f98c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -457,7 +457,11 @@ private static JsonElement ParseJsonElement(ReadOnlySpan utf8Json) if (defaultValue is null || (defaultValue == DBNull.Value && parameterType != typeof(DBNull))) { return parameterType.IsValueType - ? Activator.CreateInstance(parameterType) +#if NET + ? RuntimeHelpers.GetUninitializedObject(parameterType) +#else + ? System.Runtime.Serialization.FormatterServices.GetUninitializedObject(parameterType) +#endif : null; }