Skip to content

Commit d95df66

Browse files
jozkeeCopilot
andcommitted
Fix IndexOutOfRangeException reading inherited return-parameter description on .NET Framework
On .NET Framework, reading an inherited [Description] from the return parameter of an overriding method throws IndexOutOfRangeException: System.Attribute.GetParentDefinition indexes the result of GetParameters() with the return parameter's Position, which is -1. GetReturnParameterDescription calls GetCustomAttribute(inherit: true) on method.ReturnParameter, so any AIFunction created from such an override would throw instead of producing a schema. Modern .NET already guards this case (returning the base method's return parameter), so the failure is .NET Framework only and cannot be fixed there. Extend the existing catch filter to also treat IndexOutOfRangeException as "description absent", alongside the DynamicMethod ArgumentNullException/ NullReferenceException cases. On .NET Framework the inherited return description is silently dropped; on modern .NET it is preserved. Add a dedicated regression test with TFM-conditional assertions covering the method-level, ordinary-parameter, and return-parameter inherited descriptions on an overriding method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ec522c2 commit d95df66

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ static bool IsAsyncMethod(MethodInfo method)
10031003
// If the parameter is required and there's no argument specified for it, throw.
10041004
if (!hasDefaultValue)
10051005
{
1006-
Throw.ArgumentException(nameof(arguments), $"The arguments dictionary is missing a value for the required parameter '{parameter.Name}'.");
1006+
Throw.ArgumentException(nameof(arguments), $"The arguments dictionary is missing a value for the required parameter '{argumentName}'.");
10071007
}
10081008

10091009
// Otherwise, use the optional parameter's default value.
@@ -1221,9 +1221,11 @@ private static bool IsAIContentRelatedType(Type type) =>
12211221
{
12221222
return method.ReturnParameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description;
12231223
}
1224-
catch (Exception e) when (e is ArgumentNullException or NullReferenceException)
1224+
catch (Exception e) when (e is ArgumentNullException or NullReferenceException or IndexOutOfRangeException)
12251225
{
1226-
// DynamicMethod return parameters don't support GetCustomAttribute.
1226+
// DynamicMethod return parameters don't support GetCustomAttribute. Additionally, on .NET Framework,
1227+
// querying inherited attributes on the return parameter of an overriding method can throw
1228+
// IndexOutOfRangeException. In either case, treat the description as absent.
12271229
return null;
12281230
}
12291231
}

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#pragma warning disable IDE0004 // Remove Unnecessary Cast
1919
#pragma warning disable S103 // Lines should not be too long
2020
#pragma warning disable S107 // Methods should not have too many parameters
21+
#pragma warning disable S1144 // Unused private types or members should be removed (override methods are invoked via reflection)
2122
#pragma warning disable S2760 // Sequential tests should not check the same condition
2223
#pragma warning disable S3358 // Ternary operators should not be nested
2324
#pragma warning disable S5034 // "ValueTask" should be consumed correctly
@@ -1068,6 +1069,40 @@ public void AIFunctionFactory_ReturnTypeWithDescriptionAttribute()
10681069
static int Add(int a, int b) => a + b;
10691070
}
10701071

1072+
[Fact]
1073+
public void AIFunctionFactory_InheritedDescriptionAttributes_OnOverride()
1074+
{
1075+
MethodInfo overrideMethod = typeof(DerivedDescribed).GetMethod(nameof(DerivedDescribed.Compute))!;
1076+
AIFunction f = AIFunctionFactory.Create(overrideMethod, new DerivedDescribed());
1077+
1078+
Assert.Equal("The compute method", f.Description);
1079+
1080+
JsonElement valueParam = f.JsonSchema.GetProperty("properties").GetProperty("value");
1081+
Assert.Equal("The input value", valueParam.GetProperty("description").GetString());
1082+
1083+
Assert.NotNull(f.ReturnJsonSchema);
1084+
Assert.Equal("integer", f.ReturnJsonSchema!.Value.GetProperty("type").GetString());
1085+
#if NET
1086+
// On modern .NET the return-parameter inheritance walk is fixed, so the inherited description is read.
1087+
Assert.Equal("The computed result", f.ReturnJsonSchema!.Value.GetProperty("description").GetString());
1088+
#else
1089+
// On .NET Framework the inherited return-parameter description cannot be read and is silently dropped.
1090+
Assert.False(f.ReturnJsonSchema!.Value.TryGetProperty("description", out _));
1091+
#endif
1092+
}
1093+
1094+
private abstract class BaseDescribed
1095+
{
1096+
[Description("The compute method")]
1097+
[return: Description("The computed result")]
1098+
public abstract int Compute([Description("The input value")] int value);
1099+
}
1100+
1101+
private sealed class DerivedDescribed : BaseDescribed
1102+
{
1103+
public override int Compute(int value) => value;
1104+
}
1105+
10711106
[Fact]
10721107
public void CreateDeclaration_Roundtrips()
10731108
{

0 commit comments

Comments
 (0)