@@ -80,6 +80,16 @@ public static partial class AIFunctionFactory
80
80
/// The handling of such parameters may be overridden via <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
81
81
/// </description>
82
82
/// </item>
83
+ /// <item>
84
+ /// <description>
85
+ /// When the <see cref="AIFunction"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
86
+ /// <see cref="AIFunctionFactoryOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
87
+ /// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
88
+ /// from the <see cref="IServiceProvider"/> provided to <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>,
89
+ /// rather than from the argument collection. The handling of such parameters may be overridden via
90
+ /// <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
91
+ /// </description>
92
+ /// </item>
83
93
/// </list>
84
94
/// All other parameter types are, by default, bound from the <see cref="AIFunctionArguments"/> dictionary passed into <see cref="AIFunction.InvokeAsync"/>
85
95
/// and are included in the generated JSON schema. This may be overridden by the <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/> provided
@@ -168,6 +178,15 @@ public static AIFunction Create(Delegate method, AIFunctionFactoryOptions? optio
168
178
/// must be non-<see langword="null"/>, or else the invocation will fail with an exception due to the required nature of the parameter.
169
179
/// </description>
170
180
/// </item>
181
+ /// <item>
182
+ /// <description>
183
+ /// When the <see cref="AIFunction"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
184
+ /// <see cref="AIFunctionFactoryOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
185
+ /// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
186
+ /// from the <see cref="IServiceProvider"/> provided to <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>,
187
+ /// rather than from the argument collection.
188
+ /// </description>
189
+ /// </item>
171
190
/// </list>
172
191
/// All other parameter types are bound from the <see cref="AIFunctionArguments"/> dictionary passed into <see cref="AIFunction.InvokeAsync"/>
173
192
/// and are included in the generated JSON schema.
@@ -260,6 +279,16 @@ public static AIFunction Create(Delegate method, string? name = null, string? de
260
279
/// The handling of such parameters may be overridden via <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
261
280
/// </description>
262
281
/// </item>
282
+ /// <item>
283
+ /// <description>
284
+ /// When the <see cref="AIFunction"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
285
+ /// <see cref="AIFunctionFactoryOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
286
+ /// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
287
+ /// from the <see cref="IServiceProvider"/> provided to <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>,
288
+ /// rather than from the argument collection. The handling of such parameters may be overridden via
289
+ /// <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
290
+ /// </description>
291
+ /// </item>
263
292
/// </list>
264
293
/// All other parameter types are, by default, bound from the <see cref="AIFunctionArguments"/> dictionary passed into <see cref="AIFunction.InvokeAsync"/>
265
294
/// and are included in the generated JSON schema. This may be overridden by the <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/> provided
@@ -357,6 +386,15 @@ public static AIFunction Create(MethodInfo method, object? target, AIFunctionFac
357
386
/// <see cref="AIFunctionArguments.Services"/> is allowed to be <see langword="null"/>; otherwise, <see cref="AIFunctionArguments.Services"/>
358
387
/// must be non-<see langword="null"/>, or else the invocation will fail with an exception due to the required nature of the parameter.
359
388
/// </description>
389
+ /// <item>
390
+ /// <description>
391
+ /// When the <see cref="AIFunction"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
392
+ /// <see cref="AIFunctionFactoryOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
393
+ /// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
394
+ /// from the <see cref="IServiceProvider"/> provided to <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>,
395
+ /// rather than from the argument collection.
396
+ /// </description>
397
+ /// </item>
360
398
/// </item>
361
399
/// </list>
362
400
/// All other parameter types are bound from the <see cref="AIFunctionArguments"/> dictionary passed into <see cref="AIFunction.InvokeAsync"/>
@@ -465,6 +503,16 @@ public static AIFunction Create(MethodInfo method, object? target, string? name
465
503
/// The handling of such parameters may be overridden via <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
466
504
/// </description>
467
505
/// </item>
506
+ /// <item>
507
+ /// <description>
508
+ /// When the <see cref="AIFunction"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
509
+ /// <see cref="AIFunctionFactoryOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
510
+ /// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
511
+ /// from the <see cref="IServiceProvider"/> provided to <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>,
512
+ /// rather than from the argument collection. The handling of such parameters may be overridden via
513
+ /// <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/>.
514
+ /// </description>
515
+ /// </item>
468
516
/// </list>
469
517
/// All other parameter types are, by default, bound from the <see cref="AIFunctionArguments"/> dictionary passed into <see cref="AIFunction.InvokeAsync"/>
470
518
/// and are included in the generated JSON schema. This may be overridden by the <see cref="AIFunctionFactoryOptions.ConfigureParameterBinding"/> provided
@@ -661,7 +709,7 @@ public static ReflectionAIFunctionDescriptor GetOrCreate(MethodInfo method, AIFu
661
709
serializerOptions . MakeReadOnly ( ) ;
662
710
ConcurrentDictionary < DescriptorKey , ReflectionAIFunctionDescriptor > innerCache = _descriptorCache . GetOrCreateValue ( serializerOptions ) ;
663
711
664
- DescriptorKey key = new ( method , options . Name , options . Description , options . ConfigureParameterBinding , options . MarshalResult , schemaOptions ) ;
712
+ DescriptorKey key = new ( method , options . Name , options . Description , options . ConfigureParameterBinding , options . MarshalResult , options . Services , schemaOptions ) ;
665
713
if ( innerCache . TryGetValue ( key , out ReflectionAIFunctionDescriptor ? descriptor ) )
666
714
{
667
715
return descriptor ;
@@ -688,6 +736,8 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
688
736
}
689
737
}
690
738
739
+ IServiceProviderIsService ? serviceProviderIsService = key . Services ? . GetService < IServiceProviderIsService > ( ) ;
740
+
691
741
// Use that binding information to impact the schema generation.
692
742
AIJsonSchemaCreateOptions schemaOptions = key . SchemaOptions with
693
743
{
@@ -714,6 +764,14 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
714
764
return false ;
715
765
}
716
766
767
+ // We assume that if the services used to create the function support a particular type,
768
+ // so too do the services that will be passed into InvokeAsync. This is the same basic assumption
769
+ // made in ASP.NET.
770
+ if ( serviceProviderIsService ? . IsService ( parameterInfo . ParameterType ) is true )
771
+ {
772
+ return false ;
773
+ }
774
+
717
775
// If there was an existing IncludeParameter delegate, now defer to it as we've
718
776
// excluded everything we need to exclude.
719
777
if ( key . SchemaOptions . IncludeParameter is { } existingIncludeParameter )
@@ -735,7 +793,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions
735
793
options = default ;
736
794
}
737
795
738
- ParameterMarshallers [ i ] = GetParameterMarshaller ( serializerOptions , options , parameters [ i ] ) ;
796
+ ParameterMarshallers [ i ] = GetParameterMarshaller ( serializerOptions , options , parameters [ i ] , serviceProviderIsService ) ;
739
797
}
740
798
741
799
// Get a marshaling delegate for the return value.
@@ -805,7 +863,8 @@ static bool IsAsyncMethod(MethodInfo method)
805
863
private static Func < AIFunctionArguments , CancellationToken , object ? > GetParameterMarshaller (
806
864
JsonSerializerOptions serializerOptions ,
807
865
AIFunctionFactoryOptions . ParameterBindingOptions bindingOptions ,
808
- ParameterInfo parameter )
866
+ ParameterInfo parameter ,
867
+ IServiceProviderIsService ? serviceProviderIsService )
809
868
{
810
869
if ( string . IsNullOrWhiteSpace ( parameter . Name ) )
811
870
{
@@ -831,28 +890,28 @@ static bool IsAsyncMethod(MethodInfo method)
831
890
832
891
// We're now into default handling of everything else.
833
892
834
- // For AIFunctionArgument parameters, we bind to the arguments passed directly to InvokeAsync.
893
+ // For AIFunctionArgument parameters, we bind to the arguments passed to InvokeAsync.
835
894
if ( parameterType == typeof ( AIFunctionArguments ) )
836
895
{
837
896
return static ( arguments , _ ) => arguments ;
838
897
}
839
898
840
- // For IServiceProvider parameters, we bind to the services passed directly to InvokeAsync via AIFunctionArguments.
899
+ // For IServiceProvider parameters, we bind to the services passed to InvokeAsync via AIFunctionArguments.
841
900
if ( parameterType == typeof ( IServiceProvider ) )
842
901
{
843
902
return ( arguments , _ ) =>
844
903
{
845
904
IServiceProvider ? services = arguments . Services ;
846
- if ( services is null && ! parameter . HasDefaultValue )
905
+ if ( ! parameter . HasDefaultValue && services is null )
847
906
{
848
- Throw . ArgumentException ( nameof ( arguments ) , $ "An { nameof ( IServiceProvider ) } was not provided for the { parameter . Name } parameter." ) ;
907
+ ThrowNullServices ( parameter . Name ) ;
849
908
}
850
909
851
910
return services ;
852
911
} ;
853
912
}
854
913
855
- // For [FromKeyedServices] parameters, we bind to the services passed directly to InvokeAsync via AIFunctionArguments.
914
+ // For [FromKeyedServices] parameters, we resolve from the services passed to InvokeAsync via AIFunctionArguments.
856
915
if ( parameter . GetCustomAttribute < FromKeyedServicesAttribute > ( inherit : true ) is { } keyedAttr )
857
916
{
858
917
return ( arguments , _ ) =>
@@ -864,7 +923,38 @@ static bool IsAsyncMethod(MethodInfo method)
864
923
865
924
if ( ! parameter . HasDefaultValue )
866
925
{
867
- Throw . ArgumentException ( nameof ( arguments ) , $ "No service of type '{ parameterType } ' with key '{ keyedAttr . Key } ' was found.") ;
926
+ if ( arguments . Services is null )
927
+ {
928
+ ThrowNullServices ( parameter . Name ) ;
929
+ }
930
+
931
+ Throw . ArgumentException ( nameof ( arguments ) , $ "No service of type '{ parameterType } ' with key '{ keyedAttr . Key } ' was found for parameter '{ parameter . Name } '.") ;
932
+ }
933
+
934
+ return parameter . DefaultValue ;
935
+ } ;
936
+ }
937
+
938
+ // For any parameters that are satisfiable from the IServiceProvider, we resolve from the services passed to InvokeAsync
939
+ // via AIFunctionArguments. This is determined by the same same IServiceProviderIsService instance used to determine whether
940
+ // the parameter should be included in the schema.
941
+ if ( serviceProviderIsService ? . IsService ( parameterType ) is true )
942
+ {
943
+ return ( arguments , _ ) =>
944
+ {
945
+ if ( arguments . Services ? . GetService ( parameterType ) is { } service )
946
+ {
947
+ return service ;
948
+ }
949
+
950
+ if ( ! parameter . HasDefaultValue )
951
+ {
952
+ if ( arguments . Services is null )
953
+ {
954
+ ThrowNullServices ( parameter . Name ) ;
955
+ }
956
+
957
+ Throw . ArgumentException ( nameof ( arguments ) , $ "No service of type '{ parameterType } ' was found for parameter '{ parameter . Name } '.") ;
868
958
}
869
959
870
960
return parameter . DefaultValue ;
@@ -873,7 +963,7 @@ static bool IsAsyncMethod(MethodInfo method)
873
963
874
964
// For all other parameters, create a marshaller that tries to extract the value from the arguments dictionary.
875
965
// Resolve the contract used to marshal the value from JSON -- can throw if not supported or not found.
876
- JsonTypeInfo typeInfo = serializerOptions . GetTypeInfo ( parameterType ) ;
966
+ JsonTypeInfo ? typeInfo = serializerOptions . GetTypeInfo ( parameterType ) ;
877
967
return ( arguments , _ ) =>
878
968
{
879
969
// If the parameter has an argument specified in the dictionary, return that argument.
@@ -907,12 +997,16 @@ static bool IsAsyncMethod(MethodInfo method)
907
997
// If the parameter is required and there's no argument specified for it, throw.
908
998
if ( ! parameter . HasDefaultValue )
909
999
{
910
- Throw . ArgumentException ( nameof ( arguments ) , $ "Missing required parameter ' { parameter . Name } ' for method '{ parameter . Member . Name } '.") ;
1000
+ Throw . ArgumentException ( nameof ( arguments ) , $ "The arguments dictionary is missing a value for the required parameter '{ parameter . Name } '.") ;
911
1001
}
912
1002
913
1003
// Otherwise, use the optional parameter's default value.
914
1004
return parameter . DefaultValue ;
915
1005
} ;
1006
+
1007
+ // Throws an ArgumentNullException indicating that AIFunctionArguments.Services must be provided.
1008
+ static void ThrowNullServices ( string parameterName ) =>
1009
+ Throw . ArgumentNullException ( $ "arguments.{ nameof ( AIFunctionArguments . Services ) } ", $ "Services are required for parameter '{ parameterName } '.") ;
916
1010
}
917
1011
918
1012
/// <summary>
@@ -1075,6 +1169,7 @@ private record struct DescriptorKey(
1075
1169
string ? Description ,
1076
1170
Func < ParameterInfo , AIFunctionFactoryOptions . ParameterBindingOptions > ? GetBindParameterOptions ,
1077
1171
Func < object ? , Type ? , CancellationToken , ValueTask < object ? > > ? MarshalResult ,
1172
+ IServiceProvider ? Services ,
1078
1173
AIJsonSchemaCreateOptions SchemaOptions ) ;
1079
1174
}
1080
1175
}
0 commit comments