Skip to content

Handle DynamicMethod reflection limitations in AIFunctionFactory#7287

Merged
stephentoub merged 19 commits intomainfrom
copilot/fix-nullreferenceexception-dynamicmethod
Feb 14, 2026
Merged

Handle DynamicMethod reflection limitations in AIFunctionFactory#7287
stephentoub merged 19 commits intomainfrom
copilot/fix-nullreferenceexception-dynamicmethod

Conversation

Copy link
Contributor

Copilot AI commented Feb 11, 2026

  • Understand the issue: NullabilityInfoContext.Create(ParameterInfo) throws NullReferenceException for DynamicMethod parameters
  • Identify the fix location: AIJsonUtilities.Schema.Create.cs line 346
  • Add GetNullableWriteState helper with try/catch around nullabilityContext.Create(parameter) call with comment linking to runtime issue
  • Add test in AIFunctionFactoryTest.cs using DynamicMethod to reproduce the issue
  • Build and run targeted tests to validate the fix (all pass on net8.0, net9.0, net10.0)
  • Address review feedback: catch only NullReferenceException, restructure helper, improve test
  • Use Task<int> instead of Task<object> in DynamicMethod test
  • Remove XML doc comments from private GetNullableWriteState method
  • Return NullabilityState.Unknown only from catch; return null when context is null
  • Add back runtime PR link in catch comment
  • Fix CI test failure: replace EqualFunctionCallResults with direct JsonElement assertions
  • Fix .NET Framework CI failure: protect ReturnParameter.GetCustomAttribute with try/catch
  • Skip DynamicMethod invocation test on .NET Framework (RTDynamicMethod.Invoke unsupported)
Original 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:

  • MCP (Model Context Protocol) server integration
  • Dynamic tool generation from external schemas (OpenAPI, etc.)
  • Runtime code generation for AI function calling

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

  1. Install Microsoft.Extensions.AI 10.3.0
  2. Create a DynamicMethod with parameters:

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>));

  1. Call AIFunctionFactory.Create:

AIFunction function = AIFunctionFactory.Create(testDelegate, new AIFunctionFactoryOptions
{
Name = "TestTool",
Description = "Test tool"
});

  1. Observe NullReferenceException

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:

  • Correct JSON schema generation
  • Proper LLM tool calling
  • Meaningful debugging and logging

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

  • Version: Microsoft.Extensions.AI 10.3.0
  • Previous working version: Microsoft.Extensions.AI 10.2.0

This is confirmed regression. The exact same code works in 10.2.0 but crashes in 10.3.0.

Known Workarounds

  1. WORKAROUND (Current): Downgrade to Microsoft.Extensions.AI 10.2.0

    • Status: ✅ Works
    • Impact: Cannot use newer features/fixes from 10.3.0
  2. ATTEMPTED (Failed): Use Expression Trees instead of DynamicMethod

    • Status: ❌ Doesn't work
    • Reason: Expression Trees capture closure context, producing delegates with hidden Closure parameter:
      Func<Closure, string, int, Task> // Has extra parameter
    • This fails AIFunctionFactory validation because parameter count doesn't match expected signature
    • ATTEMPTED (Failed): Remove DefineParameter() calls

      • Status: ⚠️ Technically works but unacceptable
      • Reason: Parameters get generic names like "arg0", "arg1" which:
        • Breaks LLM tool calling (models need semantic names)
        • Breaks JSON schema generation
        • Makes debuggi...

    • ✨ 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

Copilot AI and others added 2 commits February 11, 2026 20:45
…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]>
Copilot AI changed the title [WIP] Fix NullReferenceException in AIFunctionFactory.Create with DynamicMethod Handle NullReferenceException from NullabilityInfoContext for DynamicMethod parameters Feb 11, 2026
Copilot AI requested a review from stephentoub February 11, 2026 20:53
@stephentoub stephentoub marked this pull request as ready for review February 11, 2026 21:02
@stephentoub stephentoub requested a review from a team as a code owner February 11, 2026 21:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DynamicMethod with 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.

@stephentoub
Copy link
Member

@copilot

…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]>
@stephentoub
Copy link
Member

@copilot, new CI failure on .NET Framework:

System.NullReferenceException : Object reference not set to an instance of an object.



Stack trace
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunctionDescriptor.GetReturnParameterDescription(MethodInfo method) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 1102
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunctionDescriptor..ctor(DescriptorKey key, JsonSerializerOptions serializerOptions) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 705
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunctionDescriptor.GetOrCreate(MethodInfo method, AIFunctionFactoryOptions options) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 636
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunction.Build(MethodInfo method, Object target, AIFunctionFactoryOptions options) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 498
   at Microsoft.Extensions.AI.AIFunctionFactory.Create(MethodInfo method, Object target, String name, String description, JsonSerializerOptions serializerOptions) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 356
   at Microsoft.Extensions.AI.AIFunctionFactoryTest.<AIFunctionFactory_DynamicMethod>d__70.MoveNext() in D:\a\_work\1\s\test\Libraries\Microsoft.Extensions.AI.Tests\Functions\AIFunctionFactoryTest.cs:line 1440
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

Copy link
Contributor Author

Copilot AI commented Feb 12, 2026

@copilot, new CI failure on .NET Framework:

System.NullReferenceException : Object reference not set to an instance of an object.

...

On .NET Framework, accessing ReturnParameter.Member itself throws NullReferenceException (the internal MemberImpl field is null). Replaced the .Member is null check with method is DynamicMethod, which is a reliable proactive check that avoids touching the broken ReturnParameter entirely. Fixed in ae2dd3a.

Copilot AI changed the title Handle NullReferenceException from NullabilityInfoContext for DynamicMethod parameters Handle DynamicMethod reflection limitations in AIFunctionFactory Feb 12, 2026
@stephentoub
Copy link
Member

@copilot:

Build FAILED.

/mnt/vss/_work/1/s/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs(15,1): warning S1128: Remove this unnecessary 'using'. (https://rules.sonarsource.com/csharp/RSPEC-1128) [/mnt/vss/_work/1/s/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj::TargetFramework=netstandard2.0]
/mnt/vss/_work/1/s/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs(1100,27): error CS0246: The type or namespace name 'DynamicMethod' could not be found (are you missing a using directive or an assembly reference?) [/mnt/vss/_work/1/s/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj::TargetFramework=netstandard2.0]
    1 Warning(s)
    1 Error(s)

Copy link
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot:

Build FAILED.

...

Removed the using System.Reflection.Emit and switched to method.GetType().Name == "DynamicMethod" since DynamicMethod isn't available in netstandard2.0. Fixed in b1d8c73.

@stephentoub
Copy link
Member

@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.

Copy link
Contributor Author

Copilot AI commented Feb 13, 2026

@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.

Replaced the name comparison with a try/catch that catches both ArgumentNullException (thrown on .NET Core) and NullReferenceException (thrown on .NET Framework) from ReturnParameter.GetCustomAttribute. Fixed in c00bbaf.

@stephentoub stephentoub enabled auto-merge (squash) February 13, 2026 14:08
@stephentoub
Copy link
Member

@copilot the netfx tests are still failing...

Microsoft.Extensions.AI.AIFunctionFactoryTest.AIFunctionFactory_DynamicMethod

Error message
System.ArgumentException : MethodInfo must be a runtime MethodInfo object.
Parameter name: this



Stack trace
   at System.Reflection.Emit.DynamicMethod.RTDynamicMethod.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionInvoke(MethodInfo method, Object target, Object[] arguments) in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 1227
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunction.<InvokeCoreAsync>d__28.MoveNext() in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 589
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.Extensions.AI.AIFunctionFactory.ReflectionAIFunction.<InvokeCoreAsync>d__28.MoveNext() in /_/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs:line 602
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Extensions.AI.AIFunctionFactoryTest.<AIFunctionFactory_DynamicMethod>d__70.MoveNext() in D:\a\_work\1\s\test\Libraries\Microsoft.Extensions.AI.Tests\Functions\AIFunctionFactoryTest.cs:line 1450
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)

auto-merge was automatically disabled February 13, 2026 15:42

Head branch was pushed to by a user without write access

Copy link
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot the netfx tests are still failing...

Microsoft.Extensions.AI.AIFunctionFactoryTest.AIFunctionFactory_DynamicMethod

...

On .NET Framework, RTDynamicMethod.Invoke doesn't support invocation — this is a fundamental runtime limitation (not related to our changes). Wrapped the invocation assertions in #if NET so schema generation and creation are still tested on .NET Framework, but invocation is only tested on .NET Core. Fixed in a45394b.

@stephentoub stephentoub enabled auto-merge (squash) February 13, 2026 23:07
@stephentoub stephentoub merged commit e644e1d into main Feb 14, 2026
6 checks passed
@stephentoub stephentoub deleted the copilot/fix-nullreferenceexception-dynamicmethod branch February 14, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Regression] NullReferenceException in AIFunctionFactory.Create with DynamicMethod parameters in 10.3.0

4 participants