Skip to content

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

@mdesalvo

Description

@mdesalvo

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 debugging impossible
    • Configuration

      • Microsoft.Extensions.AI: 10.3.0 (broken), 10.2.0 (works)
      • .NET Version: 10.0.0
      • OS: Mageia Linux 9
      • Use Case: MCP (Model Context Protocol) server integration
      • Architecture: x64

      Other information

      ROOT CAUSE:
      Version 10.3.0 introduced NullabilityInfoContext.Create(ParameterInfo) call in AIJsonUtilities.CreateJsonSchemaCore to extract nullability metadata. However, ParameterInfo objects from DynamicMethod.DefineParameter() lack complete RuntimeCustomAttributeData, causing the NullReferenceException.

      SUGGESTED FIX:
      Add safety check before calling NullabilityInfoContext.Create():

      try
      {
      if (NullabilityInfoContext.IsSupported)
      {
      nullabilityInfo = nullabilityContext.Create(parameterInfo);
      }
      }
      catch (NullReferenceException)
      {
      // Parameter from DynamicMethod - treat as non-nullable
      }

      Similar patterns exist in:

      IMPACT:
      This regression blocks all scenarios requiring runtime tool generation:

      • MCP server integration (GitHub's @modelcontextprotocol/server-*)
      • OpenAPI-to-AIFunction conversion
      • Plugin systems with third-party tools
      • Multi-language interop (Python/Node.js → .NET)

      WHY DYNAMICMETHOD IS ESSENTIAL:

      • Only way to get proper parameter names without closure capture
      • Most efficient for runtime code generation (per MSDN docs)
      • No assembly/type creation overhead
      • JIT-compiled code is GC-reclaimable

      Thank you for your attention to this critical regression.

Metadata

Metadata

Labels

area-aiMicrosoft.Extensions.AI librariesbugThis issue describes a behavior which is not expected - a bug.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions