From cda35c2eb37d2e210c4040ae6eb08f73abb2f162 Mon Sep 17 00:00:00 2001 From: Michael Nash Date: Tue, 23 Jun 2026 10:44:57 -0700 Subject: [PATCH 1/4] Fix partial method customization parameter renaming CreatePartialMethodFromCustomSignature passed the custom signature's parameters as the generator parameters, so the generated partial implementation's signature referenced different parameter declarations than its body and XML docs. The writer's name de-duplication then appended a numeric suffix (e.g. input0) to the signature parameters, producing code that does not compile. Pass the generated method's parameters as the generator parameters so the signature, body, and XML docs share the same parameter declarations. Also document that types generated into the same assembly must be fully qualified with global:: in a partial method declaration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ethod-parameter-rename-2026-6-23-9-50-0.md | 7 ++++ .../.tspd/docs/customization.md | 16 +++++++++ .../src/Providers/TypeProvider.cs | 4 ++- .../Custom.cs | 8 +++++ .../test/Providers/TypeProviderTests.cs | 34 +++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 .chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs diff --git a/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md b/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md new file mode 100644 index 00000000000..01e60b07f8d --- /dev/null +++ b/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Fix partial method customization so the generated implementation keeps the parameter names used by its body, instead of appending a numeric suffix (e.g. `input0`) to the signature parameters. diff --git a/packages/http-client-csharp/.tspd/docs/customization.md b/packages/http-client-csharp/.tspd/docs/customization.md index 98bb0bbbb20..b2b11c7f714 100644 --- a/packages/http-client-csharp/.tspd/docs/customization.md +++ b/packages/http-client-csharp/.tspd/docs/customization.md @@ -1302,8 +1302,24 @@ This is preferred over `[CodeGenSuppress]` + a hand-written replacement when the - **Default values on the partial implementation.** C# requires partial method implementations to have all parameters required, so any default values on your partial declaration are stripped on the generated impl. Callers still see the defaults from your declaration in custom code. - **Non-client members** (models, serialization methods, model factories). Use `[CodeGenSuppress]` for these. +### Fully qualify types generated into the same assembly + +When a parameter or return type in your partial declaration refers to a type that is generated into the **same assembly** (for example a model in the `*.Models` namespace), fully qualify it with `global::` in the declaration: + +```C# +// Instead of: static partial Response BulkDeallocateOperation(...) +static partial Response BulkDeallocateOperation( + ResourceGroupResource resourceGroupResource, + AzureLocation location, + global::Azure.ResourceManager.Compute.BulkActions.Models.ExecuteDeallocateContent content, + CancellationToken cancellationToken); +``` + +The generator reads your partial declaration before those generated types exist, so they resolve as unresolved symbols and the generator cannot recover their namespace, emitting an invalid `global::.` prefix. Qualifying with `global::.` lets the generator recover the namespace and reduce it correctly in the generated file. Types that already exist when the declaration is read (framework types, types from referenced packages) do not need qualification. +
+ **Generated code before (Generated/TestClient.cs):** ```C# diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 7a3047886b6..3e3de9d157b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -413,8 +413,10 @@ internal MethodProvider[] FilterCustomizedMethods(IEnumerable sp private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSignature customSignature, MethodProvider generatedMethod) { // Partial method implementations require all parameters to be required (no default values). + // The generator's parameters carry the metadata and the declarations referenced by the + // method body and XML docs; the custom signature only supplies the parameter names. var requiredParameters = PartialMethodCustomization.RenameAndCloneParameters( - customSignature.Parameters, + generatedMethod.Signature.Parameters, customSignature.Parameters, removeDefaults: true); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs new file mode 100644 index 00000000000..1d44cc7bc8a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs @@ -0,0 +1,8 @@ +#nullable disable + +namespace Test; + +public partial class CustomPartialMethodType +{ + static partial void MyMethod(string input); +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index e597456fef9..ea526753c96 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -433,6 +433,40 @@ public async Task TestSpecViewReturnsAllMethodsEvenWhenCustomized() Assert.AreEqual("Method3", specView.Methods[2].Signature.Name); } + [Test] + public async Task CustomPartialMethodImplementationKeepsParameterNames() + { + await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputParam = new ParameterProvider("input", $"The input value.", typeof(string)); + var generatedMethod = new MethodProvider( + new MethodSignature( + "MyMethod", + $"Does something.", + MethodSignatureModifiers.Static, + null, + null, + [inputParam]), + inputParam.Invoke("ToString").Terminate(), + new TestTypeProvider()); + + var typeProvider = new TestTypeProvider(name: "CustomPartialMethodType", methods: [generatedMethod]); + + var method = typeProvider.Methods.Single(); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + + using var codeWriter = new CodeWriter(); + codeWriter.WriteMethod(method); + var result = codeWriter.ToString(false); + + // The partial implementation's signature parameter must keep the same name the body uses. + // Regression guard: the parameter must not be renamed with a numeric suffix (e.g. "input0"). + Assert.IsFalse( + result.Contains("input0"), + $"Partial implementation renamed the parameter with a numeric suffix:\n{result}"); + Assert.AreEqual("input", method.Signature.Parameters.Single().Name); + } + [Test] public async Task TestSpecViewReturnsAllPropertiesEvenWhenSuppressed() { From 264c6756855210e4b9d1c72e00732f6e9fc4b92d Mon Sep 17 00:00:00 2001 From: Michael Nash Date: Tue, 23 Jun 2026 11:05:24 -0700 Subject: [PATCH 2/4] Remove chronus entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...fix-partial-method-parameter-rename-2026-6-23-9-50-0.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md diff --git a/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md b/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md deleted file mode 100644 index 01e60b07f8d..00000000000 --- a/.chronus/changes/fix-partial-method-parameter-rename-2026-6-23-9-50-0.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-csharp" ---- - -Fix partial method customization so the generated implementation keeps the parameter names used by its body, instead of appending a numeric suffix (e.g. `input0`) to the signature parameters. From d1530cd2070b11fcd8ecd57f719d9f1938d76e22 Mon Sep 17 00:00:00 2001 From: Michael Nash Date: Tue, 23 Jun 2026 11:44:01 -0700 Subject: [PATCH 3/4] Fix prettier formatting in customization.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/.tspd/docs/customization.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-csharp/.tspd/docs/customization.md b/packages/http-client-csharp/.tspd/docs/customization.md index b2b11c7f714..decca27acfa 100644 --- a/packages/http-client-csharp/.tspd/docs/customization.md +++ b/packages/http-client-csharp/.tspd/docs/customization.md @@ -1319,7 +1319,6 @@ The generator reads your partial declaration before those generated types exist,
- **Generated code before (Generated/TestClient.cs):** ```C# From b6f0f1639f4c0d7828816e6cd38f8002d4b14f98 Mon Sep 17 00:00:00 2001 From: jolov Date: Tue, 23 Jun 2026 11:53:22 -0700 Subject: [PATCH 4/4] Use generator return type for partial method customization When a partial method declaration's return type references a type generated into the same assembly, the type is unresolved when the declaration is read (Roslyn error type with no namespace), producing malformed 'global::.TypeName' output. BuildPartialSignature now accepts the generator's resolved return type, and CreatePartialMethodFromCustomSignature passes it. C# requires a partial method's declaration and implementation to share the same return type, so the generator's resolved type is necessarily correct. Removes the docs note that asked users to qualify return types with global::. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.tspd/docs/customization.md | 15 ------ .../Providers/PartialMethodCustomization.cs | 13 ++++- .../src/Providers/TypeProvider.cs | 5 +- .../Custom.cs | 11 ++++ .../test/Providers/TypeProviderTests.cs | 50 +++++++++++++++++++ 5 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs diff --git a/packages/http-client-csharp/.tspd/docs/customization.md b/packages/http-client-csharp/.tspd/docs/customization.md index decca27acfa..98bb0bbbb20 100644 --- a/packages/http-client-csharp/.tspd/docs/customization.md +++ b/packages/http-client-csharp/.tspd/docs/customization.md @@ -1302,21 +1302,6 @@ This is preferred over `[CodeGenSuppress]` + a hand-written replacement when the - **Default values on the partial implementation.** C# requires partial method implementations to have all parameters required, so any default values on your partial declaration are stripped on the generated impl. Callers still see the defaults from your declaration in custom code. - **Non-client members** (models, serialization methods, model factories). Use `[CodeGenSuppress]` for these. -### Fully qualify types generated into the same assembly - -When a parameter or return type in your partial declaration refers to a type that is generated into the **same assembly** (for example a model in the `*.Models` namespace), fully qualify it with `global::` in the declaration: - -```C# -// Instead of: static partial Response BulkDeallocateOperation(...) -static partial Response BulkDeallocateOperation( - ResourceGroupResource resourceGroupResource, - AzureLocation location, - global::Azure.ResourceManager.Compute.BulkActions.Models.ExecuteDeallocateContent content, - CancellationToken cancellationToken); -``` - -The generator reads your partial declaration before those generated types exist, so they resolve as unresolved symbols and the generator cannot recover their namespace, emitting an invalid `global::.` prefix. Qualifying with `global::.` lets the generator recover the namespace and reduce it correctly in the generated file. Types that already exist when the declaration is read (framework types, types from referenced packages) do not need qualification. -
**Generated code before (Generated/TestClient.cs):** diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs index 549f9e6a283..1d5682d7f09 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs @@ -166,9 +166,18 @@ public static IReadOnlyList RenameAndCloneParameters( /// The customer's partial declaration signature. /// The parameters to use for the implementation. /// Must all be required (no default values) per the C# partial method rules. + /// The return type the implementation should use. Pass the + /// generator's own return type rather than relying on the customer's parsed declaration: + /// the customer's declaration may reference types generated into the same assembly, which + /// are unresolved when the declaration is read (Roslyn surfaces them as error types with no + /// namespace), producing malformed global::.TypeName output. C# requires a partial + /// method's declaration and implementation to share the same return type, so the generator's + /// resolved return type is necessarily the correct one. When null, the customer's + /// parsed return type is used as a fallback. public static MethodSignature BuildPartialSignature( MethodSignature customSignature, - IReadOnlyList implementationParameters) + IReadOnlyList implementationParameters, + CSharpType? returnType = null) { if (customSignature is null) { @@ -184,7 +193,7 @@ public static MethodSignature BuildPartialSignature( customSignature.Name, customSignature.Description, customSignature.Modifiers | MethodSignatureModifiers.Partial, - customSignature.ReturnType, + returnType ?? customSignature.ReturnType, customSignature.ReturnDescription, implementationParameters, customSignature.Attributes, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 3e3de9d157b..5b1af563dff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -420,7 +420,10 @@ private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSigna customSignature.Parameters, removeDefaults: true); - var partialSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredParameters); + var partialSignature = PartialMethodCustomization.BuildPartialSignature( + customSignature, + requiredParameters, + generatedMethod.Signature.ReturnType); MethodProvider partialMethod = generatedMethod.BodyExpression != null ? new MethodProvider(partialSignature, generatedMethod.BodyExpression, generatedMethod.EnclosingType, generatedMethod.XmlDocs, generatedMethod.Suppressions) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs new file mode 100644 index 00000000000..740e8c32c0a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs @@ -0,0 +1,11 @@ +#nullable disable + +namespace Test; + +public partial class CustomPartialReturnType +{ + // CustomReturnModel is a type generated into the same assembly, so it is unresolved when this + // declaration is read. The generated implementation must still use the generator's resolved + // return type (with its namespace) rather than this unresolved one. + static partial CustomReturnModel MyMethod(string input); +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index ea526753c96..9ec0b21c32d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -467,6 +467,56 @@ public async Task CustomPartialMethodImplementationKeepsParameterNames() Assert.AreEqual("input", method.Signature.Parameters.Single().Name); } + [Test] + public async Task CustomPartialMethodImplementationUsesGeneratorReturnType() + { + await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The generator's resolved return type carries a namespace. The custom partial + // declaration references the same type by name, but it is unresolved when read (a + // type generated into the same assembly), so its parsed CSharpType has no namespace. + var generatorReturnType = new CSharpType( + "CustomReturnModel", + "Sample.Models", + isValueType: false, + isNullable: false, + declaringType: null, + args: [], + isPublic: true, + isStruct: false); + + var inputParam = new ParameterProvider("input", $"The input value.", typeof(string)); + var generatedMethod = new MethodProvider( + new MethodSignature( + "MyMethod", + $"Does something.", + MethodSignatureModifiers.Static, + generatorReturnType, + $"The result.", + [inputParam]), + inputParam.Invoke("ToString").Terminate(), + new TestTypeProvider()); + + var typeProvider = new TestTypeProvider(name: "CustomPartialReturnType", methods: [generatedMethod]); + + var method = typeProvider.Methods.Single(); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + + // The implementation must use the generator's resolved return type (with namespace), + // not the customer's unresolved parsed return type (empty namespace). + Assert.AreEqual("Sample.Models", method.Signature.ReturnType!.Namespace); + Assert.AreEqual("CustomReturnModel", method.Signature.ReturnType.Name); + + using var codeWriter = new CodeWriter(); + codeWriter.WriteMethod(method); + var result = codeWriter.ToString(false); + + // Regression guard: an empty-namespace return type renders as the malformed `global::.`. + Assert.IsFalse( + result.Contains("global::."), + $"Partial implementation wrote a return type with no namespace:\n{result}"); + } + [Test] public async Task TestSpecViewReturnsAllPropertiesEvenWhenSuppressed() {