From 489e147818014f84a7de5cc98c6f270507edb180 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 17 Apr 2026 13:04:18 +0000 Subject: [PATCH 1/4] Honor UnmappedMemberHandling in AIFunctionFactory parameter binding When JsonSerializerOptions.UnmappedMemberHandling is set to Disallow, AIFunction invocations will now throw ArgumentException if the provided AIFunctionArguments contains keys that do not correspond to any declared parameter of the underlying method. This mirrors STJ's handling of unmapped properties for object deserialization and enables opt-in strict validation of tool call arguments. Addresses discussion in modelcontextprotocol/csharp-sdk#1508. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Functions/AIFunctionFactory.cs | 38 +++++++++++++++ .../Functions/AIFunctionFactoryOptions.cs | 10 ++++ .../Functions/AIFunctionFactoryTest.cs | 46 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 9398805da9a..898cf92f211 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -15,6 +15,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; using System.Threading; @@ -627,6 +628,25 @@ private ReflectionAIFunction( var paramMarshallers = FunctionDescriptor.ParameterMarshallers; object?[] args = paramMarshallers.Length != 0 ? new object?[paramMarshallers.Length] : []; + // If the configured serializer options request strict handling of unmapped members, + // verify that every argument key corresponds to a declared parameter name. This mirrors + // JsonSerializerOptions.UnmappedMemberHandling behavior for object deserialization by + // applying the same policy to top-level AIFunction argument binding. + if (FunctionDescriptor.JsonSerializerOptions.UnmappedMemberHandling == JsonUnmappedMemberHandling.Disallow && + arguments.Count > 0 && + FunctionDescriptor.ExpectedArgumentNames is { } expectedNames) + { + foreach (KeyValuePair kvp in arguments) + { + if (!expectedNames.Contains(kvp.Key)) + { + Throw.ArgumentException( + nameof(arguments), + $"The arguments dictionary contains an unexpected key '{kvp.Key}' that does not correspond to any parameter of '{Name}'."); + } + } + } + for (int i = 0; i < args.Length; i++) { args[i] = paramMarshallers[i](arguments, cancellationToken); @@ -733,6 +753,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions // Get marshaling delegates for parameters. ParameterMarshallers = parameters.Length > 0 ? new Func[parameters.Length] : []; + HashSet? expectedArgumentNames = null; for (int i = 0; i < parameters.Length; i++) { if (boundParameters?.TryGetValue(parameters[i], out AIFunctionFactoryOptions.ParameterBindingOptions options) is not true) @@ -741,8 +762,24 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions } ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i]); + + // Collect the set of parameter names that are potentially sourced from the arguments dictionary. + // Infrastructure parameters (CancellationToken, AIFunctionArguments, IServiceProvider) are always + // bound from dedicated sources and are never resolved by argument name, so they are excluded. + // Custom BindParameter callbacks may read arbitrary keys from the arguments dictionary, so we + // include their parameter name as a permitted argument name rather than flagging it as unmapped. + Type pType = parameters[i].ParameterType; + if (pType != typeof(CancellationToken) && + pType != typeof(AIFunctionArguments) && + pType != typeof(IServiceProvider) && + !string.IsNullOrEmpty(parameters[i].Name)) + { + _ = (expectedArgumentNames ??= new HashSet(StringComparer.Ordinal)).Add(parameters[i].Name!); + } } + ExpectedArgumentNames = expectedArgumentNames; + ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType); Method = key.Method; Name = key.Name ?? key.Method.GetCustomAttribute(inherit: true)?.DisplayName ?? GetFunctionName(key.Method); @@ -770,6 +807,7 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions public JsonElement? ReturnJsonSchema { get; } public Func[] ParameterMarshallers { get; } public Func> ReturnParameterMarshaller { get; } + public HashSet? ExpectedArgumentNames { get; } public ReflectionAIFunction? CachedDefaultInstance { get; set; } private static string GetFunctionName(MethodInfo method) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index bcb552bf242..fa8c46d45b4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -25,7 +25,17 @@ public AIFunctionFactoryOptions() /// Gets or sets the used to marshal .NET values being passed to the underlying delegate. /// + /// /// If no value has been specified, the instance will be used. + /// + /// + /// When is set to + /// , the produced + /// will throw at invocation time if the + /// dictionary contains keys that do not correspond to any of the function's parameters. This mirrors + /// the handling of unmapped properties during object deserialization and enables strict validation of + /// tool call arguments. + /// /// public JsonSerializerOptions? SerializerOptions { get; set; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index e373f77aa5a..08df0a3202c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -1455,6 +1455,52 @@ public async Task AIFunctionFactory_DynamicMethod() #endif } + [Fact] + public async Task Parameters_UnmappedMemberHandlingDisallow_ThrowsOnExtraArgument_Async() + { + JsonSerializerOptions strictOptions = new(AIJsonUtilities.DefaultOptions) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + + AIFunction func = AIFunctionFactory.Create( + (string taskId, string update, bool markComplete = false) => $"{taskId}:{update}:{markComplete}", + new AIFunctionFactoryOptions { SerializerOptions = strictOptions }); + + // Extra, unrecognized argument causes a throw. + ArgumentException ex = await Assert.ThrowsAsync("arguments", async () => + await func.InvokeAsync(new() + { + ["taskId"] = "abc", + ["update"] = "Done", + ["phase"] = "completed", + })); + Assert.Contains("phase", ex.Message); + + // Still succeeds when no unexpected arguments are present (optional parameter omitted). + object? result = await func.InvokeAsync(new() + { + ["taskId"] = "abc", + ["update"] = "Done", + }); + AssertExtensions.EqualFunctionCallResults("abc:Done:False", result); + } + + [Fact] + public async Task Parameters_UnmappedMemberHandlingDefault_IgnoresExtraArgument_Async() + { + // Default behavior (Skip) should preserve pre-existing lenient binding. + AIFunction func = AIFunctionFactory.Create( + (string update, bool markComplete = false) => $"{update}:{markComplete}"); + + object? result = await func.InvokeAsync(new() + { + ["update"] = "Done", + ["phase"] = "completed", + }); + AssertExtensions.EqualFunctionCallResults("Done:False", result); + } + [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] [JsonSerializable(typeof(string))] From 09fe0c1e1eaf57b479b74e2fa5fd3873d778c3d7 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 17 Apr 2026 13:10:12 +0000 Subject: [PATCH 2/4] Use is pattern and honor AIFunctionArguments comparer for match check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Functions/AIFunctionFactory.cs | 33 +++++++++++++++---- .../Functions/AIFunctionFactoryTest.cs | 32 ++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 898cf92f211..0393a8d0220 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -631,20 +631,39 @@ private ReflectionAIFunction( // If the configured serializer options request strict handling of unmapped members, // verify that every argument key corresponds to a declared parameter name. This mirrors // JsonSerializerOptions.UnmappedMemberHandling behavior for object deserialization by - // applying the same policy to top-level AIFunction argument binding. - if (FunctionDescriptor.JsonSerializerOptions.UnmappedMemberHandling == JsonUnmappedMemberHandling.Disallow && + // applying the same policy to top-level AIFunction argument binding. Argument name matching + // honors the comparer of the supplied AIFunctionArguments dictionary (ordinal by default). + if (FunctionDescriptor.JsonSerializerOptions.UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow && arguments.Count > 0 && FunctionDescriptor.ExpectedArgumentNames is { } expectedNames) { - foreach (KeyValuePair kvp in arguments) + int matched = 0; + foreach (string name in expectedNames) { - if (!expectedNames.Contains(kvp.Key)) + if (arguments.ContainsKey(name)) { - Throw.ArgumentException( - nameof(arguments), - $"The arguments dictionary contains an unexpected key '{kvp.Key}' that does not correspond to any parameter of '{Name}'."); + matched++; } } + + if (matched != arguments.Count) + { + foreach (KeyValuePair kvp in arguments) + { + if (!expectedNames.Contains(kvp.Key)) + { + Throw.ArgumentException( + nameof(arguments), + $"The arguments dictionary contains an unexpected key '{kvp.Key}' that does not correspond to any parameter of '{Name}'."); + } + } + + // Fallback for comparer mismatches (e.g. case-insensitive arguments dictionary + // with duplicate-casing keys aliasing to the same parameter). + Throw.ArgumentException( + nameof(arguments), + $"The arguments dictionary contains keys that do not correspond to any parameter of '{Name}'."); + } } for (int i = 0; i < args.Length; i++) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 08df0a3202c..09a43e94781 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -1501,6 +1501,38 @@ public async Task Parameters_UnmappedMemberHandlingDefault_IgnoresExtraArgument_ AssertExtensions.EqualFunctionCallResults("Done:False", result); } + [Fact] + public async Task Parameters_UnmappedMemberHandlingDisallow_HonorsArgumentsComparer_Async() + { + JsonSerializerOptions strictOptions = new(AIJsonUtilities.DefaultOptions) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + + AIFunction func = AIFunctionFactory.Create( + (string update, bool markComplete = false) => $"{update}:{markComplete}", + new AIFunctionFactoryOptions { SerializerOptions = strictOptions }); + + // Case-insensitive arguments dictionary: casing variations of the parameter name must not be + // flagged as unmapped, since the binding lookup itself is case-insensitive. + AIFunctionArguments caseInsensitive = new(StringComparer.OrdinalIgnoreCase) + { + ["UPDATE"] = "Done", + ["MarkComplete"] = true, + }; + AssertExtensions.EqualFunctionCallResults("Done:True", await func.InvokeAsync(caseInsensitive)); + + // A genuinely unmapped key is still flagged even with a case-insensitive comparer. + AIFunctionArguments withExtra = new(StringComparer.OrdinalIgnoreCase) + { + ["update"] = "Done", + ["PHASE"] = "completed", + }; + ArgumentException ex = await Assert.ThrowsAsync("arguments", async () => + await func.InvokeAsync(withExtra)); + Assert.Contains("PHASE", ex.Message); + } + [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] [JsonSerializable(typeof(string))] From a99fda56f70e9b1a4c4f076400ea1bf29147ccf6 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 17 Apr 2026 13:19:38 +0000 Subject: [PATCH 3/4] Address PR feedback: parameterless methods, BindParameter bypass, docs - Always build the expected-argument-name set (even if empty) so strict mode also flags extra keys on parameterless methods / methods with only infrastructure parameters. - Skip strict unmapped-key validation when any parameter uses a custom ParameterBindingOptions.BindParameter callback, since those may legitimately consume arbitrary argument keys. - Expanded the XML docs on AIFunctionFactoryOptions.SerializerOptions to clarify which parameter names are considered valid and that custom parameter binding bypasses the strict check. - Added tests for both new behaviors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Functions/AIFunctionFactory.cs | 29 ++++++++--- .../Functions/AIFunctionFactoryOptions.cs | 13 ++++- .../Functions/AIFunctionFactoryTest.cs | 50 +++++++++++++++++++ 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs index 0393a8d0220..4625b168aa6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs @@ -633,10 +633,15 @@ private ReflectionAIFunction( // JsonSerializerOptions.UnmappedMemberHandling behavior for object deserialization by // applying the same policy to top-level AIFunction argument binding. Argument name matching // honors the comparer of the supplied AIFunctionArguments dictionary (ordinal by default). + // + // Validation is skipped when custom ParameterBindingOptions.BindParameter callbacks are in + // use, since those may legitimately source values from argument keys that do not correspond + // to the .NET parameter names. if (FunctionDescriptor.JsonSerializerOptions.UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow && arguments.Count > 0 && - FunctionDescriptor.ExpectedArgumentNames is { } expectedNames) + !FunctionDescriptor.HasCustomParameterBinding) { + HashSet expectedNames = FunctionDescriptor.ExpectedArgumentNames; int matched = 0; foreach (string name in expectedNames) { @@ -772,7 +777,8 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions // Get marshaling delegates for parameters. ParameterMarshallers = parameters.Length > 0 ? new Func[parameters.Length] : []; - HashSet? expectedArgumentNames = null; + HashSet expectedArgumentNames = new(StringComparer.Ordinal); + bool hasCustomParameterBinding = false; for (int i = 0; i < parameters.Length; i++) { if (boundParameters?.TryGetValue(parameters[i], out AIFunctionFactoryOptions.ParameterBindingOptions options) is not true) @@ -782,22 +788,30 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i]); + if (options.BindParameter is not null) + { + // Custom BindParameter callbacks can legally source their value from arbitrary keys in the + // AIFunctionArguments dictionary, so we cannot know in advance which keys are "expected". + // Note this down so that strict unmapped-member validation is skipped in InvokeCoreAsync. + hasCustomParameterBinding = true; + } + // Collect the set of parameter names that are potentially sourced from the arguments dictionary. // Infrastructure parameters (CancellationToken, AIFunctionArguments, IServiceProvider) are always - // bound from dedicated sources and are never resolved by argument name, so they are excluded. - // Custom BindParameter callbacks may read arbitrary keys from the arguments dictionary, so we - // include their parameter name as a permitted argument name rather than flagging it as unmapped. + // bound from dedicated sources and are never resolved by argument name, so they are excluded from + // the permitted set. Type pType = parameters[i].ParameterType; if (pType != typeof(CancellationToken) && pType != typeof(AIFunctionArguments) && pType != typeof(IServiceProvider) && !string.IsNullOrEmpty(parameters[i].Name)) { - _ = (expectedArgumentNames ??= new HashSet(StringComparer.Ordinal)).Add(parameters[i].Name!); + _ = expectedArgumentNames.Add(parameters[i].Name!); } } ExpectedArgumentNames = expectedArgumentNames; + HasCustomParameterBinding = hasCustomParameterBinding; ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions, out Type? returnType); Method = key.Method; @@ -826,7 +840,8 @@ private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions public JsonElement? ReturnJsonSchema { get; } public Func[] ParameterMarshallers { get; } public Func> ReturnParameterMarshaller { get; } - public HashSet? ExpectedArgumentNames { get; } + public HashSet ExpectedArgumentNames { get; } + public bool HasCustomParameterBinding { get; } public ReflectionAIFunction? CachedDefaultInstance { get; set; } private static string GetFunctionName(MethodInfo method) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index fa8c46d45b4..2c07543e41b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -32,9 +32,18 @@ public AIFunctionFactoryOptions() /// When is set to /// , the produced /// will throw at invocation time if the - /// dictionary contains keys that do not correspond to any of the function's parameters. This mirrors + /// dictionary contains keys that do not correspond to a bindable function parameter. This mirrors /// the handling of unmapped properties during object deserialization and enables strict validation of - /// tool call arguments. + /// tool call arguments. For this validation, only parameters populated from + /// are considered valid argument names; infrastructure parameters such as , + /// , and are excluded, so argument keys + /// matching those parameter names are still treated as unexpected even if the underlying method declares + /// parameters with those names. + /// + /// + /// This strict validation is based on the function's parameter metadata and is not applied to functions + /// that use custom parameter binding configured through , since + /// such callbacks may legitimately source parameter values from arbitrary argument keys. /// /// public JsonSerializerOptions? SerializerOptions { get; set; } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs index 09a43e94781..1bd803aded3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs @@ -1533,6 +1533,56 @@ public async Task Parameters_UnmappedMemberHandlingDisallow_HonorsArgumentsCompa Assert.Contains("PHASE", ex.Message); } + [Fact] + public async Task Parameters_UnmappedMemberHandlingDisallow_ParameterlessMethod_ThrowsOnAnyArgument_Async() + { + JsonSerializerOptions strictOptions = new(AIJsonUtilities.DefaultOptions) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + + AIFunction func = AIFunctionFactory.Create( + () => "ok", + new AIFunctionFactoryOptions { SerializerOptions = strictOptions }); + + // No args is fine. + AssertExtensions.EqualFunctionCallResults("ok", await func.InvokeAsync()); + + // Any extra key is flagged. + ArgumentException ex = await Assert.ThrowsAsync("arguments", async () => + await func.InvokeAsync(new() { ["phase"] = "completed" })); + Assert.Contains("phase", ex.Message); + } + + [Fact] + public async Task Parameters_UnmappedMemberHandlingDisallow_CustomBindParameter_SkipsStrictValidation_Async() + { + JsonSerializerOptions strictOptions = new(AIJsonUtilities.DefaultOptions) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + + // A custom BindParameter callback sources its value from a key that does not correspond + // to the .NET parameter name. Strict validation must be skipped so such binders keep working. + AIFunction func = AIFunctionFactory.Create( + (string update) => $"update:{update}", + new AIFunctionFactoryOptions + { + SerializerOptions = strictOptions, + ConfigureParameterBinding = _ => new() + { + BindParameter = (_, args) => args["aliasedKey"], + }, + }); + + object? result = await func.InvokeAsync(new() + { + ["aliasedKey"] = "hello", + ["anotherKey"] = "world", + }); + AssertExtensions.EqualFunctionCallResults("update:hello", result); + } + [JsonSerializable(typeof(IAsyncEnumerable))] [JsonSerializable(typeof(int[]))] [JsonSerializable(typeof(string))] From cf89f9584615b6c731312da5179d57324c702a3e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 17 Apr 2026 13:44:04 +0000 Subject: [PATCH 4/4] Condense SerializerOptions remark on UnmappedMemberHandling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Functions/AIFunctionFactoryOptions.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs index 2c07543e41b..094bb09337a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs @@ -29,21 +29,10 @@ public AIFunctionFactoryOptions() /// If no value has been specified, the instance will be used. /// /// - /// When is set to - /// , the produced - /// will throw at invocation time if the - /// dictionary contains keys that do not correspond to a bindable function parameter. This mirrors - /// the handling of unmapped properties during object deserialization and enables strict validation of - /// tool call arguments. For this validation, only parameters populated from - /// are considered valid argument names; infrastructure parameters such as , - /// , and are excluded, so argument keys - /// matching those parameter names are still treated as unexpected even if the underlying method declares - /// parameters with those names. - /// - /// - /// This strict validation is based on the function's parameter metadata and is not applied to functions - /// that use custom parameter binding configured through , since - /// such callbacks may legitimately source parameter values from arbitrary argument keys. + /// The setting is honored by the function parameter + /// binder: when set to , invoking + /// the produced throws if the supplied contains keys + /// that do not correspond to a bindable parameter of the underlying method. /// /// public JsonSerializerOptions? SerializerOptions { get; set; }