Handle DynamicMethod reflection limitations in AIFunctionFactory#7287
Handle DynamicMethod reflection limitations in AIFunctionFactory#7287stephentoub merged 19 commits intomainfrom
Conversation
…thod parameters Wrap NullabilityInfoContext.Create(ParameterInfo) call in a try/catch to handle parameters that lack complete reflection metadata, such as those created via DynamicMethod.DefineParameter(). This is a workaround for a runtime issue tracked at dotnet/runtime#124293. Co-authored-by: stephentoub <[email protected]>
…n comment and test invocation Co-authored-by: stephentoub <[email protected]>
src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs
Outdated
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs
Outdated
Show resolved
Hide resolved
test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs
Outdated
Show resolved
Hide resolved
test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Fixes a regression where AIFunctionFactory.Create() can crash when generating JSON schema for delegates created from DynamicMethod.DefineParameter() due to a runtime NullabilityInfoContext bug, and adds a regression test to cover the scenario.
Changes:
- Add a helper to safely retrieve parameter nullability write-state, swallowing runtime exceptions and treating nullability as unknown.
- Add a regression test that builds a
DynamicMethodwith named parameters, validates schema generation, and verifies invocation succeeds.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs | Routes nullable-reference-type parameter handling through a new helper that guards against runtime exceptions from incomplete reflection metadata. |
| test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs | Adds a regression test for DynamicMethod.DefineParameter() parameters to ensure schema generation and invocation don’t throw. |
src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs
Show resolved
Hide resolved
…re helper, improve test - Remove ArgumentException and InvalidOperationException from catch filter - Restructure to if-not-null with early return pattern per review - Add S1696 pragma suppression for intentional NullReferenceException catch - Rewrite test to use DynamicMethod that doubles an int and validates result Co-authored-by: stephentoub <[email protected]>
test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: stephentoub <[email protected]>
src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs
Outdated
Show resolved
Hide resolved
src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs
Show resolved
Hide resolved
|
@copilot, new CI failure on .NET Framework: |
…mber Co-authored-by: stephentoub <[email protected]>
... On .NET Framework, accessing |
|
…compatibility Co-authored-by: stephentoub <[email protected]>
|
@copilot, I don't want to use a name comparison. If there's really no other way, we can fall back to the try/catch you'd previously proposed. |
Co-authored-by: stephentoub <[email protected]>
Replaced the name comparison with a try/catch that catches both |
|
@copilot the netfx tests are still failing... |
….Invoke is unsupported Co-authored-by: stephentoub <[email protected]>
Head branch was pushed to by a user without write access
... On .NET Framework, |
NullabilityInfoContext.Create(ParameterInfo)throwsNullReferenceExceptionforDynamicMethodparametersAIJsonUtilities.Schema.Create.csline 346GetNullableWriteStatehelper with try/catch aroundnullabilityContext.Create(parameter)call with comment linking to runtime issueAIFunctionFactoryTest.csusingDynamicMethodto reproduce the issueTask<int>instead ofTask<object>in DynamicMethod testGetNullableWriteStatemethodNullabilityState.Unknownonly from catch; returnnullwhen context is nullReturnParameter.GetCustomAttributewith try/catchOriginal prompt
This section details on the original issue you should resolve
<issue_title>[Regression] NullReferenceException in AIFunctionFactory.Create with DynamicMethod parameters in 10.3.0</issue_title>
<issue_description>### Description
Microsoft.Extensions.AI version 10.3.0 throws NullReferenceException when calling AIFunctionFactory.Create() with delegates created via DynamicMethod.DefineParameter(). This is a regression - the same code works perfectly in version 10.2.0.
The crash occurs in NullabilityInfoContext.Create(ParameterInfo) when it tries to read RuntimeCustomAttributeData from parameters created with DynamicMethod, which lack complete reflection metadata.
This completely blocks legitimate use cases like:
DynamicMethod is the only way to create delegates with proper parameter names and types without closure capture (Expression Trees add hidden Closure parameter that breaks AIFunctionFactory validation).
Reproduction Steps
using System.Reflection;
using System.Reflection.Emit;
using Microsoft.Extensions.AI;
DynamicMethod dynamicMethod = new DynamicMethod(
"TestMethod",
typeof(Task),
new[] { typeof(string), typeof(int) },
typeof(Program).Module);
dynamicMethod.DefineParameter(1, ParameterAttributes.None, "message");
dynamicMethod.DefineParameter(2, ParameterAttributes.None, "count");
ILGenerator il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Call, typeof(Task).GetMethod(nameof(Task.FromResult)).MakeGenericMethod(typeof(object)));
il.Emit(OpCodes.Ret);
Delegate testDelegate = dynamicMethod.CreateDelegate(typeof(Func<string, int, Task>));
AIFunction function = AIFunctionFactory.Create(testDelegate, new AIFunctionFactoryOptions
{
Name = "TestTool",
Description = "Test tool"
});
Expected behavior
AIFunctionFactory.Create() should successfully create an AIFunction from the DynamicMethod delegate, just as it did in version 10.2.0.
The function should have proper parameter names ("message", "count") derived from DefineParameter() calls, enabling:
Actual behavior
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Reflection.RuntimeCustomAttributeData.GetCustomAttributeRecords(RuntimeModule module, Int32 targetToken)
at System.Reflection.RuntimeCustomAttributeData.GetCustomAttributes(RuntimeModule module, Int32 tkTarget)
at System.Reflection.NullabilityInfoContext.Create(ParameterInfo parameterInfo)
at Microsoft.Extensions.AI.AIJsonUtilities.<>c__DisplayClass42_0.g__TransformSchemaNode|0(JsonSchemaExporterContext schemaExporterContext, JsonNode schema)
at System.Text.Json.Schema.JsonSchema.g__CompleteSchema|100_0(JsonNode schema, <>c__DisplayClass100_0&)
at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
at System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(JsonTypeInfo typeInfo, JsonSchemaExporterOptions exporterOptions)
at Microsoft.Extensions.AI.AIJsonUtilities.CreateJsonSchemaCore(...)
at Microsoft.Extensions.AI.AIJsonUtilities.CreateFunctionJsonSchema(...)
at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunctionDescriptor..ctor(...)
at Microsoft.Extensions.AI.AIFunctionFactory.Create(Delegate method, AIFunctionFactoryOptions options)
The crash occurs because DynamicMethod.DefineParameter() creates ParameterInfo with incomplete RuntimeCustomAttributeData, which NullabilityInfoContext cannot handle.
Regression?
☑ Yes
This is confirmed regression. The exact same code works in 10.2.0 but crashes in 10.3.0.
Known Workarounds
WORKAROUND (Current): Downgrade to Microsoft.Extensions.AI 10.2.0
ATTEMPTED (Failed): Use Expression Trees instead of DynamicMethod
Func<Closure, string, int, Task> // Has extra parameter
ATTEMPTED (Failed): Remove DefineParameter() calls
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.
Microsoft Reviewers: Open in CodeFlow