From 82780a6ddacc3681eba90051905922e4175d926e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:57:15 +0000 Subject: [PATCH 1/6] Initial plan From f7d04d99fc770953abe940029c5a5b26fbc80258 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:15:30 +0000 Subject: [PATCH 2/6] fix: deduplicate sub-clients in GetSubClients to prevent ToDictionary crash with duplicate children Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b15af653-c208-49a5-bd0c-c7bef5ab6a6b Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 2 +- .../ClientProviders/ClientProviderTests.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 5b6d76d93b6..461019c2f94 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -1506,7 +1506,7 @@ private IReadOnlyList GetSubClients() foreach (var client in _inputClient.Children) { var subClient = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); - if (subClient != null) + if (subClient != null && !subClients.Contains(subClient)) { subClients.Add(subClient); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index c18cf79e9ff..b5264e4a26f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -1084,6 +1084,38 @@ public void TestBuildFields_ForParent_InitializedByParentOnly_WithSubClientParam Assert.IsNull(cachingField, "Parent should not have caching field for subclient that has subclient-specific parameters in its accessor"); } + [Test] + public void TestBuildMethods_ForParent_DuplicateChildrenDoesNotCrash() + { + var parentClient = InputFactory.Client("ParentClient"); + var subClient = InputFactory.Client( + "SubClient", + parent: parentClient, + initializedBy: InputClientInitializedBy.Parent); + + // Simulate duplicate children (same InputClient appearing multiple times in children list) + parentClient.Update(children: [subClient, subClient]); + + MockHelpers.LoadMockGenerator( + clients: () => [parentClient]); + + var parentProvider = new ClientProvider(parentClient); + + Assert.IsNotNull(parentProvider); + + // Accessing Methods should not throw due to duplicate children + Assert.DoesNotThrow(() => { var _ = parentProvider.Methods; }); + + // The parent should have only one factory method for the subclient (not duplicated) + var factoryMethods = parentProvider.Methods.Where( + m => m.Signature?.Name == "GetSubClient" || m.Signature?.Name == "GetSubClientClient").ToList(); + Assert.AreEqual(1, factoryMethods.Count, "Parent should have exactly one factory method for the subclient despite duplicate children"); + + // The parent should have only one caching field (not duplicated) + var cachingFields = parentProvider.Fields.Where(f => f.Name == "_cachedSubClient").ToList(); + Assert.AreEqual(1, cachingFields.Count, "Parent should have exactly one caching field despite duplicate children"); + } + private void ValidatePrimaryConstructor( ConstructorProvider primaryPublicConstructor, List inputParameters, From 3b6159d9bb461b508bd4bd073fb116f5eb4634d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:20:31 +0000 Subject: [PATCH 3/6] refactor: use HashSet for O(1) dedup lookup in GetSubClients Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b15af653-c208-49a5-bd0c-c7bef5ab6a6b Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ClientProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 461019c2f94..04b7a79b2f8 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -1501,12 +1501,13 @@ private ParameterProvider BuildClientEndpointParameter() private IReadOnlyList GetSubClients() { + var seen = new HashSet(); var subClients = new List(_inputClient.Children.Count); foreach (var client in _inputClient.Children) { var subClient = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); - if (subClient != null && !subClients.Contains(subClient)) + if (subClient != null && seen.Add(subClient)) { subClients.Add(subClient); } From 4cd06363254155571026e216ae410cba6d624a15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:06:47 +0000 Subject: [PATCH 4/6] fix: deduplicate children in emitter client-converter instead of generator Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/01cbad23-efa3-4bc8-b1cb-9aa2ec7d8f59 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../emitter/src/lib/client-converter.ts | 13 +++- .../test/Unit/client-converter.test.ts | 74 +++++++++++++++++++ .../src/Providers/ClientProvider.cs | 3 +- .../ClientProviders/ClientProviderTests.cs | 32 -------- 4 files changed, 84 insertions(+), 38 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/lib/client-converter.ts b/packages/http-client-csharp/emitter/src/lib/client-converter.ts index 52586f2ddf0..710aeefb1f4 100644 --- a/packages/http-client-csharp/emitter/src/lib/client-converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/client-converter.ts @@ -106,11 +106,16 @@ function fromSdkClient( fromSdkClient(sdkContext, client.parent, rootApiVersions), ); } - // fill children + // fill children, deduplicating to avoid duplicate entries in the code model if (client.children) { - inputClient.children = client.children.map((c) => - diagnostics.pipe(fromSdkClient(sdkContext, c, rootApiVersions)), - ); + const seen = new Set(); + inputClient.children = client.children + .map((c) => diagnostics.pipe(fromSdkClient(sdkContext, c, rootApiVersions))) + .filter((c) => { + if (seen.has(c)) return false; + seen.add(c); + return true; + }); } return diagnostics.wrap(inputClient); diff --git a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts index 77c21d09e02..8a2f4d40bad 100644 --- a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts @@ -458,3 +458,77 @@ describe("client name suffix", () => { } }); }); + +describe("client children deduplication", () => { + let runner: TestHost; + + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + it("should not have duplicate children in the code model", async () => { + const program = await typeSpecCompile( + ` + @service(#{ + title: "Test Service", + }) + @server( + "{endpoint}/client/structure/{client}", + "", + { + endpoint: url, + client: string, + } + ) + namespace TestService { + @route("/one") + @post + op one(): void; + + @route("/two") + @post + op two(): void; + + interface Foo { + @route("/three") + @post + three(): void; + } + } + + @client({ + name: "FirstClient", + service: TestService, + }) + namespace TestClientNs { + op one is TestService.one; + + @client + interface Group3 { + two is TestService.two; + three is TestService.Foo.three; + } + } + `, + runner, + { IsNamespaceNeeded: false, IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const [root] = createModel(sdkContext); + + const client = root.clients[0]; + ok(client, "Client should exist"); + + if (client.children && client.children.length > 0) { + // Verify no duplicates by checking unique crossLanguageDefinitionIds + const childIds = client.children.map((c) => c.crossLanguageDefinitionId); + const uniqueIds = new Set(childIds); + strictEqual( + childIds.length, + uniqueIds.size, + `Client children should have no duplicates. Found: [${childIds.join(", ")}]`, + ); + } + }); +}); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 04b7a79b2f8..5b6d76d93b6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -1501,13 +1501,12 @@ private ParameterProvider BuildClientEndpointParameter() private IReadOnlyList GetSubClients() { - var seen = new HashSet(); var subClients = new List(_inputClient.Children.Count); foreach (var client in _inputClient.Children) { var subClient = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); - if (subClient != null && seen.Add(subClient)) + if (subClient != null) { subClients.Add(subClient); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index b5264e4a26f..c18cf79e9ff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -1084,38 +1084,6 @@ public void TestBuildFields_ForParent_InitializedByParentOnly_WithSubClientParam Assert.IsNull(cachingField, "Parent should not have caching field for subclient that has subclient-specific parameters in its accessor"); } - [Test] - public void TestBuildMethods_ForParent_DuplicateChildrenDoesNotCrash() - { - var parentClient = InputFactory.Client("ParentClient"); - var subClient = InputFactory.Client( - "SubClient", - parent: parentClient, - initializedBy: InputClientInitializedBy.Parent); - - // Simulate duplicate children (same InputClient appearing multiple times in children list) - parentClient.Update(children: [subClient, subClient]); - - MockHelpers.LoadMockGenerator( - clients: () => [parentClient]); - - var parentProvider = new ClientProvider(parentClient); - - Assert.IsNotNull(parentProvider); - - // Accessing Methods should not throw due to duplicate children - Assert.DoesNotThrow(() => { var _ = parentProvider.Methods; }); - - // The parent should have only one factory method for the subclient (not duplicated) - var factoryMethods = parentProvider.Methods.Where( - m => m.Signature?.Name == "GetSubClient" || m.Signature?.Name == "GetSubClientClient").ToList(); - Assert.AreEqual(1, factoryMethods.Count, "Parent should have exactly one factory method for the subclient despite duplicate children"); - - // The parent should have only one caching field (not duplicated) - var cachingFields = parentProvider.Fields.Where(f => f.Name == "_cachedSubClient").ToList(); - Assert.AreEqual(1, cachingFields.Count, "Parent should have exactly one caching field despite duplicate children"); - } - private void ValidatePrimaryConstructor( ConstructorProvider primaryPublicConstructor, List inputParameters, From 07b25f5835554a3aa9552d4183caa062144cbf79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:19:12 +0000 Subject: [PATCH 5/6] refactor: use loop with Set for dedup and add TODO for upstream fix Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/cd61d1eb-8886-47b1-8549-b54a5a329c54 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../emitter/src/lib/client-converter.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/lib/client-converter.ts b/packages/http-client-csharp/emitter/src/lib/client-converter.ts index 710aeefb1f4..2dd4c81a2cf 100644 --- a/packages/http-client-csharp/emitter/src/lib/client-converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/client-converter.ts @@ -106,16 +106,17 @@ function fromSdkClient( fromSdkClient(sdkContext, client.parent, rootApiVersions), ); } - // fill children, deduplicating to avoid duplicate entries in the code model + // fill children + // TODO: remove deduplication once https://github.com/Azure/typespec-azure/issues/4251 is fixed if (client.children) { - const seen = new Set(); - inputClient.children = client.children - .map((c) => diagnostics.pipe(fromSdkClient(sdkContext, c, rootApiVersions))) - .filter((c) => { - if (seen.has(c)) return false; - seen.add(c); - return true; - }); + const seen = new Set(); + const children: InputClient[] = []; + for (const child of client.children) { + if (seen.has(child)) continue; + seen.add(child); + children.push(diagnostics.pipe(fromSdkClient(sdkContext, child, rootApiVersions))); + } + inputClient.children = children; } return diagnostics.wrap(inputClient); From 10ef6a75165000b69534ac39469bed8cac818e84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:33:37 +0000 Subject: [PATCH 6/6] test: add additional nested interface to TestClientNs in dedup test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/fb528503-05e4-4f56-bbb0-51ea4188d6ae Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../emitter/test/Unit/client-converter.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts index 8a2f4d40bad..9d5a1b36c03 100644 --- a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts @@ -494,6 +494,12 @@ describe("client children deduplication", () => { @post three(): void; } + + interface Bar { + @route("/four") + @post + four(): void; + } } @client({ @@ -508,6 +514,11 @@ describe("client children deduplication", () => { two is TestService.two; three is TestService.Foo.three; } + + @client + interface Group4 { + four is TestService.Bar.four; + } } `, runner,