From 6eb86ca219de8cf9c80b353b95b985fcff36077e Mon Sep 17 00:00:00 2001 From: jolov Date: Sun, 22 Mar 2026 11:59:24 -0700 Subject: [PATCH 1/4] fix: match spread parameters by wire name instead of C# name When constructing spread models in convenience methods, the parameter matching between convenience method parameters and model constructor parameters used C# names which can diverge due to: - @clientName renames (e.g., 'model' -> 'OverrideModelName') - @encodedName wire names leaking (e.g., 'tool_resources' vs 'toolResources') - PascalCase vs camelCase differences This caused unmatched parameters to fall back to 'default' (null), leading to NullReferenceException during serialization when Optional.IsCollectionDefined(null) returns true for null collections. Fix: match by the property's wire (serialized) name, which is stable across both the convenience parameter and the model property since they represent the same JSON field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Providers/ScmMethodProviderCollection.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index 9b8fb7433dc..d48f2913050 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -278,7 +278,18 @@ private IEnumerable GetStackVariablesForProtocolParamConver private List GetSpreadConversion(TypeProvider spreadSource) { - var convenienceMethodParams = ConvenienceMethodParameters.ToDictionary(p => p.Name); + // Match convenience method parameters to constructor parameters by wire (serialized) name. + // This handles cases where C# names diverge due to @clientName renames, @encodedName, + // or casing differences between the convenience parameters and model properties. + var convenienceMethodParamsByWireName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var p in ConvenienceMethodParameters) + { + if (p.WireInfo?.SerializedName != null) + { + convenienceMethodParamsByWireName.TryAdd(p.WireInfo.SerializedName, p); + } + } + List expressions = new(spreadSource.Properties.Count); // we should make this find more deterministic var ctor = spreadSource.CanonicalView.Constructors.First(c => @@ -287,7 +298,10 @@ private List GetSpreadConversion(TypeProvider spreadSource) foreach (var param in ctor.Signature.Parameters) { - if (convenienceMethodParams.TryGetValue(param.Name, out var convenienceParam)) + var wireName = param.Property?.WireInfo?.SerializedName; + var matched = wireName != null && convenienceMethodParamsByWireName.TryGetValue(wireName, out var convenienceParam); + + if (matched) { if (convenienceParam.Type.IsList) { From b5eeea1d981f30623f7fc8328d8704f90c69d724 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 23 Mar 2026 10:38:12 -0700 Subject: [PATCH 2/4] fix: resolve CS8602/CS0165 nullable warnings in spread parameter matching Move the TryGetValue into the if condition directly so the compiler can track that convenienceParam is definitely assigned when used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ScmMethodProviderCollection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index d48f2913050..9821ac2993a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -299,9 +299,8 @@ private List GetSpreadConversion(TypeProvider spreadSource) foreach (var param in ctor.Signature.Parameters) { var wireName = param.Property?.WireInfo?.SerializedName; - var matched = wireName != null && convenienceMethodParamsByWireName.TryGetValue(wireName, out var convenienceParam); - if (matched) + if (wireName != null && convenienceMethodParamsByWireName.TryGetValue(wireName, out var convenienceParam)) { if (convenienceParam.Type.IsList) { From bb43c9bd4579b5553999e8a1cae160c357d8e4af Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 23 Mar 2026 11:18:58 -0700 Subject: [PATCH 3/4] regen: update test project baselines after spread fix The Sample-TypeSpec test project now correctly forwards the 'name' parameter in spread model construction instead of using 'default'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Sample-TypeSpec/src/Generated/SampleTypeSpecClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecClient.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecClient.cs index 78cb15103f5..69abeb54bd5 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecClient.cs @@ -719,7 +719,7 @@ public virtual ClientResult AnonymousBody(string name, BinaryData require Argument.AssertNotNullOrEmpty(propertyWithSpecialDocs, nameof(propertyWithSpecialDocs)); Thing spreadModel = new Thing( - default, + name, requiredUnion, "accept", requiredNullableString, @@ -770,7 +770,7 @@ public virtual async Task> AnonymousBodyAsync(string name, B Argument.AssertNotNullOrEmpty(propertyWithSpecialDocs, nameof(propertyWithSpecialDocs)); Thing spreadModel = new Thing( - default, + name, requiredUnion, "accept", requiredNullableString, From a65a97c6697da857cabffdc27ae9deb84e700874 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 23 Mar 2026 12:09:00 -0700 Subject: [PATCH 4/4] fix: fall back to name-based matching when wire name unavailable The wire-name-only matching fails for cases where the constructor parameter's Property.WireInfo is not available (e.g., in unit tests using mock models). Add a fallback that matches by C# name (case-insensitive) when wire name matching doesn't find a match. Matching order: 1. Wire name (handles @clientName, @encodedName, casing differences) 2. C# name (handles simple cases and parameters without wire info) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Providers/ScmMethodProviderCollection.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index 9821ac2993a..dfbc4a42423 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -278,16 +278,19 @@ private IEnumerable GetStackVariablesForProtocolParamConver private List GetSpreadConversion(TypeProvider spreadSource) { - // Match convenience method parameters to constructor parameters by wire (serialized) name. - // This handles cases where C# names diverge due to @clientName renames, @encodedName, + // Match convenience method parameters to constructor parameters by wire (serialized) name + // to handle cases where C# names diverge due to @clientName renames, @encodedName, // or casing differences between the convenience parameters and model properties. + // Falls back to C# name matching for parameters without wire information. var convenienceMethodParamsByWireName = new Dictionary(StringComparer.OrdinalIgnoreCase); + var convenienceMethodParamsByName = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var p in ConvenienceMethodParameters) { if (p.WireInfo?.SerializedName != null) { convenienceMethodParamsByWireName.TryAdd(p.WireInfo.SerializedName, p); } + convenienceMethodParamsByName.TryAdd(p.Name, p); } List expressions = new(spreadSource.Properties.Count); @@ -299,8 +302,12 @@ private List GetSpreadConversion(TypeProvider spreadSource) foreach (var param in ctor.Signature.Parameters) { var wireName = param.Property?.WireInfo?.SerializedName; + if (!(wireName != null && convenienceMethodParamsByWireName.TryGetValue(wireName, out var convenienceParam))) + { + convenienceMethodParamsByName.TryGetValue(param.Name, out convenienceParam); + } - if (wireName != null && convenienceMethodParamsByWireName.TryGetValue(wireName, out var convenienceParam)) + if (convenienceParam != null) { if (convenienceParam.Type.IsList) {