Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c772354
Initial plan
Copilot Feb 11, 2026
a28a1db
Fix NullReferenceException in AIFunctionFactory.Create with DynamicMe…
Copilot Feb 11, 2026
d33d09b
Address code review feedback: add NullReferenceException justificatio…
Copilot Feb 11, 2026
77477e9
Address review feedback: catch only NullReferenceException, restructu…
Copilot Feb 11, 2026
d2cd15f
Use Task<int> instead of Task<object> in DynamicMethod test
Copilot Feb 11, 2026
26139fb
Remove XML doc comments from GetNullableWriteState private method
Copilot Feb 12, 2026
930857f
Return NullabilityState.Unknown instead of null from GetNullableWrite…
Copilot Feb 12, 2026
6567c48
Return null when context is null, NullabilityState.Unknown only from …
Copilot Feb 12, 2026
b17c517
Add back runtime PR link in catch comment
Copilot Feb 12, 2026
d397aea
Fix CI test failure: replace EqualFunctionCallResults with direct ass…
Copilot Feb 12, 2026
8cd06f8
Fix ArgumentNullException from ReturnParameter.GetCustomAttribute on …
Copilot Feb 12, 2026
063997f
Replace try/catch with proactive null check on ReturnParameter.Member
Copilot Feb 12, 2026
ae2dd3a
Check method is DynamicMethod instead of accessing ReturnParameter.Me…
Copilot Feb 12, 2026
81011d3
Merge branch 'main' into copilot/fix-nullreferenceexception-dynamicme…
stephentoub Feb 13, 2026
b1d8c73
Use type name check instead of DynamicMethod type for netstandard2.0 …
Copilot Feb 13, 2026
c00bbaf
Replace name comparison with try/catch for DynamicMethod ReturnParameter
Copilot Feb 13, 2026
9b063e1
Merge branch 'main' into copilot/fix-nullreferenceexception-dynamicme…
stephentoub Feb 13, 2026
a45394b
Skip DynamicMethod invocation test on .NET Framework where MethodInfo…
Copilot Feb 13, 2026
bd608c6
Merge branch 'main' into copilot/fix-nullreferenceexception-dynamicme…
stephentoub Feb 14, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
JsonSerializerOptions = serializerOptions;
ReturnJsonSchema = returnType is null || key.ExcludeResultSchema ? null : AIJsonUtilities.CreateJsonSchema(
NormalizeReturnType(returnType, serializerOptions),
description: key.Method.ReturnParameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
description: GetReturnParameterDescription(key.Method),
serializerOptions: serializerOptions,
inferenceOptions: schemaOptions);

Expand Down Expand Up @@ -1093,6 +1093,19 @@ private static bool IsAIContentRelatedType(Type type) =>
typeof(AIContent).IsAssignableFrom(type) ||
typeof(IEnumerable<AIContent>).IsAssignableFrom(type);

private static string? GetReturnParameterDescription(MethodInfo method)
{
try
{
return method.ReturnParameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description;
}
catch (Exception e) when (e is ArgumentNullException or NullReferenceException)
{
// DynamicMethod return parameters don't support GetCustomAttribute.
return null;
}
}

private static Type NormalizeReturnType(Type type, JsonSerializerOptions? options)
{
options ??= AIJsonUtilities.DefaultOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#pragma warning disable S1075 // URIs should not be hardcoded
#pragma warning disable S1199 // Nested block
#pragma warning disable S1696 // NullReferenceException should not be caught
#pragma warning disable SA1118 // Parameter should not span multiple lines

namespace Microsoft.Extensions.AI;
Expand Down Expand Up @@ -343,7 +344,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
}
else if (parameter is not null &&
Comment thread
stephentoub marked this conversation as resolved.
!ctx.TypeInfo.Type.IsValueType &&
nullabilityContext?.Create(parameter).WriteState is NullabilityState.Nullable)
GetNullableWriteState(nullabilityContext, parameter) is NullabilityState.Nullable)
{
// Handle nullable reference type parameters (e.g., object?).
if (objSchema.TryGetPropertyValue(TypePropertyName, out JsonNode? typeKeyWord) &&
Expand Down Expand Up @@ -833,4 +834,23 @@ internal static bool TryGetEffectiveDefaultValue(ParameterInfo parameterInfo, ou

return defaultValue;
}

private static NullabilityState? GetNullableWriteState(NullabilityInfoContext? nullabilityContext, ParameterInfo parameter)
{
if (nullabilityContext is not null)
{
try
{
return nullabilityContext.Create(parameter).WriteState;
Comment thread
stephentoub marked this conversation as resolved.
}
catch (NullReferenceException)
{
// NullabilityInfoContext can throw for parameters that lack complete reflection metadata
// (e.g. DynamicMethod parameters). cf. https://github.com/dotnet/runtime/pull/124293
return NullabilityState.Unknown;
}
}

return null;
Comment thread
stephentoub marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -1418,6 +1419,42 @@ public void JsonSchema_NullableReferenceTypeParameters_AllowNull()
Assert.DoesNotContain("nullableIntWithDefault", requiredParams);
}

[Fact]
public async Task AIFunctionFactory_DynamicMethod()
{
DynamicMethod dynamicMethod = new DynamicMethod(
"DoubleIt",
typeof(Task<int>),
new[] { typeof(int) },
typeof(AIFunctionFactoryTest).Module);

dynamicMethod.DefineParameter(1, ParameterAttributes.None, "value");

ILGenerator il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4_2);
il.Emit(OpCodes.Mul);
il.Emit(OpCodes.Call, typeof(Task).GetMethod(nameof(Task.FromResult))!.MakeGenericMethod(typeof(int)));
il.Emit(OpCodes.Ret);

Delegate testDelegate = dynamicMethod.CreateDelegate(typeof(Func<int, Task<int>>));

AIFunction func = AIFunctionFactory.Create(testDelegate.GetMethodInfo(), testDelegate.Target);

Assert.Equal("DoubleIt", func.Name);

JsonElement schema = func.JsonSchema;
JsonElement properties = schema.GetProperty("properties");
Assert.True(properties.TryGetProperty("value", out _));

#if NET
// DynamicMethod invocation via MethodInfo.Invoke is not supported on .NET Framework.
object? result = await func.InvokeAsync(new() { ["value"] = 21 });
Assert.IsType<JsonElement>(result);
Assert.Equal(42, ((JsonElement)result!).GetInt32());
#endif
}
Comment thread
stephentoub marked this conversation as resolved.

[JsonSerializable(typeof(IAsyncEnumerable<int>))]
[JsonSerializable(typeof(int[]))]
[JsonSerializable(typeof(string))]
Expand Down
Loading