Repository: microsoft/kiota (csharp)

Issue:
Chained builder does not work when parameters change names for subrequests
I am trying to generate a client for the ConnectWise REST API.

In that API are two get requests, one inside the other:
```
/finance/agreements/{id}
/finance/agreements/{parentId}/additions/{id}
```

When building the second request with a C# generated client, the chained syntax does not work:
```csharp
client.Finance.Agreements["1"].Additions["2"]
```

This gives a dictionary key conflict exception:
"An item with the same key has already been added. Key: id"

This is because `client.Finance.Agreements["1"]` added a key called id to the dictionary with `id:1`, and then `Additions["2"]` tried to add a key to the same dictionary (a new dictionary based on the existing `PathParameters`) with the same name, `id:2`, but actually that parameter changed names during the building process.

You could argue that it's a problem with the API design, but this is a large public API that can't be changed by the consumer, and as far as I know, it's allowed within the OpenAPI spec.

This is not just a problem of conflicts, but also changing names when building subrequests - even if there was not a conflict, the name change would still break it.

There is a work around to create the builder from scratch:
```csharp
new ApiSdk.Finance.Agreements.Item.Additions.Item.AdditionsItemRequestBuilder(
    new Dictionary<string, object> { { "parentId", "1" }, { "id", "2" }},
    requestAdapter)
```
but it would be nicer if the chained syntax could handle this e.g. by adjusting the key names when `PathParameters` is passed to the subrequest, or by building the value into the base url for the subrequest.

Pull request:
fixes #3757

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9db905a57..61cabd0020 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 
+- Fixed a bug where path parameters deduplication would create collisions on sub path segments. [#3757](https://github.com/microsoft/kiota/issues/3757)
 - Moved from net7 to net8.
 - Fixed a bug where import statements for additionalDataHolder and enumSet are missing when BackingStore is enabled in java. [#3643](https://github.com/microsoft/kiota/pull/3643)  
 - Fixed a bug where getBackingStore method body was malformed for java. [#3643](https://github.com/microsoft/kiota/pull/3643)  
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index 5fe1a230ed..083c576a3e 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -549,10 +549,15 @@ private void MergeIndexNodesAtSameLevel(OpenApiUrlTreeNode node)
         if (indexNodes.Length > 1)
         {
             var indexNode = indexNodes[0];
+            node.Children.Remove(indexNode.Key);
+            var newSegmentParameterName = $"{{{node.Segment.CleanupSymbolName()}-id}}";
+            indexNode.Value.Path = indexNode.Value.Path.Replace(indexNode.Key, newSegmentParameterName, StringComparison.OrdinalIgnoreCase);
+            node.Children.Add(newSegmentParameterName, indexNode.Value);
+            CopyNodeIntoOtherNode(indexNode.Value, indexNode.Value, indexNode.Key, newSegmentParameterName);
             foreach (var child in indexNodes.Except(new[] { indexNode }))
             {
                 node.Children.Remove(child.Key);
-                CopyNodeIntoOtherNode(child.Value, indexNode.Value, child.Key, indexNode.Key);
+                CopyNodeIntoOtherNode(child.Value, indexNode.Value, child.Key, newSegmentParameterName);
             }
         }
 
@@ -567,8 +572,8 @@ private void CopyNodeIntoOtherNode(OpenApiUrlTreeNode source, OpenApiUrlTreeNode
             if (!destination.Children.TryAdd(child.Key, child.Value))
                 CopyNodeIntoOtherNode(child.Value, destination.Children[child.Key], pathParameterNameToReplace, pathParameterNameReplacement);
         }
-        pathParameterNameToReplace = pathParameterNameToReplace.TrimStart('{').TrimEnd('}');
-        pathParameterNameReplacement = pathParameterNameReplacement.TrimStart('{').TrimEnd('}');
+        pathParameterNameToReplace = pathParameterNameToReplace.Trim('{', '}');
+        pathParameterNameReplacement = pathParameterNameReplacement.Trim('{', '}');
         foreach (var pathItem in source.PathItems)
         {
             foreach (var pathParameter in pathItem
@@ -585,7 +590,7 @@ private void CopyNodeIntoOtherNode(OpenApiUrlTreeNode source, OpenApiUrlTreeNode
             {
                 pathParameter.Name = pathParameterNameReplacement;
             }
-            if (!destination.PathItems.TryAdd(pathItem.Key, pathItem.Value))
+            if (source != destination && !destination.PathItems.TryAdd(pathItem.Key, pathItem.Value))
             {
                 var destinationPathItem = destination.PathItems[pathItem.Key];
                 foreach (var operation in pathItem.Value.Operations)


Test patch:
diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
index 61ae73c1a3..76d25f805b 100644
--- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
+++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
@@ -6334,6 +6334,25 @@ public void SinglePathParametersAreDeduplicated()
         {
             Paths = new OpenApiPaths
             {
+                ["users/{foo}/careerAdvisor/{id}"] = new OpenApiPathItem
+                {
+                    Operations = {
+                        [OperationType.Get] = new OpenApiOperation
+                        {
+                            Responses = new OpenApiResponses {
+                                ["200"] = new OpenApiResponse
+                                {
+                                    Content = {
+                                        ["application/json"] = new OpenApiMediaType
+                                        {
+                                            Schema = userSchema
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
                 ["users/{id}/careerAdvisor"] = new OpenApiPathItem
                 {
                     Operations = {
@@ -6390,12 +6409,17 @@ public void SinglePathParametersAreDeduplicated()
         Assert.NotNull(managerRB);
         var managerUrlTemplate = managerRB.FindChildByName<CodeProperty>("UrlTemplate", false);
         Assert.NotNull(managerUrlTemplate);
-        Assert.Equal("{+baseurl}/users/{id}/manager", managerUrlTemplate.DefaultValue.Trim('"'));
+        Assert.Equal("{+baseurl}/users/{users%2Did}/manager", managerUrlTemplate.DefaultValue.Trim('"'));
         var careerAdvisorRB = codeModel.FindNamespaceByName("ApiSdk.users.item.careerAdvisor").FindChildByName<CodeClass>("CareerAdvisorRequestBuilder", false);
         Assert.NotNull(careerAdvisorRB);
         var careerAdvisorUrlTemplate = careerAdvisorRB.FindChildByName<CodeProperty>("UrlTemplate", false);
         Assert.NotNull(careerAdvisorUrlTemplate);
-        Assert.Equal("{+baseurl}/users/{id}/careerAdvisor", careerAdvisorUrlTemplate.DefaultValue.Trim('"'));
+        Assert.Equal("{+baseurl}/users/{users%2Did}/careerAdvisor", careerAdvisorUrlTemplate.DefaultValue.Trim('"'));
+        var careerAdvisorItemRB = codeModel.FindNamespaceByName("ApiSdk.users.item.careerAdvisor.item").FindChildByName<CodeClass>("CareerAdvisorItemRequestBuilder", false);
+        Assert.NotNull(careerAdvisorItemRB);
+        var careerAdvisorItemUrlTemplate = careerAdvisorItemRB.FindChildByName<CodeProperty>("UrlTemplate", false);
+        Assert.NotNull(careerAdvisorItemUrlTemplate);
+        Assert.Equal("{+baseurl}/users/{users%2Did}/careerAdvisor/{id}", careerAdvisorItemUrlTemplate.DefaultValue.Trim('"'));
     }
     [Fact]
     public void AddReservedPathParameterSymbol()