Repository: microsoft/kiota (csharp)

Issue:
Change how required parameters are represented in URI Templates
Currently all parameters are rendered in URI templates like this:

```
  {+baseUrl}{?paramA,paramB,paramC}
```

I would like us to consider changing the way required parameters are presenting.  Consider paramA and paramB are optional, but paramC is required, the URI Template would be 

```
  {+baseUrl}?paramC={paramC}{&paramA,paramB}
```

With this approach, query parameters that are required but allow an empty value, do not require the application developer to explicitly set the empty value.  
The other advantage is that when using this URI Template for documentation/debugging purposes it is very clear which parameters are required vs optional.
This approach also produces a more stable URL as the order of query parameters is impacted less due to the presence of optional parameters.  This is helpful to prevent HTTP cache pollution.

Pull request:
fixes #3989

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef1ae24c9f..4039250434 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed serialization of scalar members in union types for Python. [#2828](https://github.com/microsoft/kiota/issues/2828)
 - Fixed a bug where scalar error mappings would be generated even though it's not supported by the http request adapter. [#4018](https://github.com/microsoft/kiota/issues/4018)
 - Switched to proxy generation for TypeScript, leading to about ~44% bundle sizes reduction. [#3642](https://github.com/microsoft/kiota/issues/3642)
+- Required query parameters are now projected as `{baseurl+}foo/bar?required={required}` instead of `{baseurl+}foo/bar{?required}` so they are automatically populated if no value is provided. [#3989](https://github.com/microsoft/kiota/issues/3989)
 - Fixed a bug where TypeScript models factory methods would be missing return types.
 - Fixed a bug where generated paths would possibly get too long. [#3854](https://github.com/microsoft/kiota/issues/3854)
 - The vscode extension now also displays the children nodes when filtering. [#3998](https://github.com/microsoft/kiota/issues/3998)
diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
index 9e45a8ba83..11a4e9d30e 100644
--- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
+++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
@@ -194,16 +194,23 @@ public static string GetUrlTemplate(this OpenApiUrlTreeNode currentNode)
                                         pathItem.Operations
                                                 .SelectMany(static x => x.Value.Parameters)
                                                 .Where(static x => x.In == ParameterLocation.Query))
-                                    .DistinctBy(static x => x.Name)
+                                    .DistinctBy(static x => x.Name, StringComparer.Ordinal)
+                                    .OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)
                                     .ToArray();
             if (parameters.Length != 0)
-                queryStringParameters = "{?" +
-                                        parameters.Select(static x =>
+            {
+                var requiredParameters = string.Join("&", parameters.Where(static x => x.Required)
+                                                .Select(static x =>
+                                                            $"{x.Name}={{{x.Name.SanitizeParameterNameForUrlTemplate()}}}"));
+                var optionalParameters = string.Join(",", parameters.Where(static x => !x.Required)
+                                                .Select(static x =>
                                                             x.Name.SanitizeParameterNameForUrlTemplate() +
                                                             (x.Explode ?
-                                                                "*" : string.Empty))
-                                                .Aggregate(static (x, y) => $"{x},{y}") +
-                                        '}';
+                                                                "*" : string.Empty)));
+                var hasRequiredParameters = !string.IsNullOrEmpty(requiredParameters);
+                var hasOptionalParameters = !string.IsNullOrEmpty(optionalParameters);
+                queryStringParameters = $"{(hasRequiredParameters ? "?" : string.Empty)}{requiredParameters}{(hasOptionalParameters ? "{" : string.Empty)}{(hasOptionalParameters && hasRequiredParameters ? "&" : string.Empty)}{(hasOptionalParameters && !hasRequiredParameters ? "?" : string.Empty)}{optionalParameters}{(hasOptionalParameters ? "}" : string.Empty)}";
+            }
         }
         var pathReservedPathParametersIds = currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pItem) ?
                                                 pItem.Parameters


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
index 8d662fb894..2ad8c46606 100644
--- a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
+++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs
@@ -131,13 +131,13 @@ public void GetUrlTemplateSelectsDistinctQueryParameters()
     {
         var doc = new OpenApiDocument
         {
-            Paths = new(),
+            Paths = [],
         };
         doc.Paths.Add("{param-with-dashes}\\existing-segment", new()
         {
             Operations = new Dictionary<OperationType, OpenApiOperation> {
                 { OperationType.Get, new() {
-                        Parameters = new List<OpenApiParameter> {
+                        Parameters = [
                             new() {
                                 Name = "param-with-dashes",
                                 In = ParameterLocation.Path,
@@ -155,12 +155,12 @@ public void GetUrlTemplateSelectsDistinctQueryParameters()
                                 },
                                 Style = ParameterStyle.Simple,
                             }
-                        }
+                        ]
                     }
                 },
                 {
                     OperationType.Put, new() {
-                        Parameters = new List<OpenApiParameter> {
+                        Parameters = [
                             new() {
                                 Name = "param-with-dashes",
                                 In = ParameterLocation.Path,
@@ -178,7 +178,7 @@ public void GetUrlTemplateSelectsDistinctQueryParameters()
                                 },
                                 Style = ParameterStyle.Simple,
                             }
-                        }
+                        ]
                     }
                 }
             }
@@ -187,19 +187,280 @@ public void GetUrlTemplateSelectsDistinctQueryParameters()
         Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select}", node.Children.First().Value.GetUrlTemplate());
         // the query parameters will be decoded by a middleware at runtime before the request is executed
     }
+    [Fact]
+    public void GeneratesRequiredQueryParametersAndOptionalMixInPathItem()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Parameters = {
+                        new OpenApiParameter {
+                            Name = "id",
+                            In = ParameterLocation.Path,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "filter",
+                            In = ParameterLocation.Query,
+                            Required = false,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "apikey",
+                            In = ParameterLocation.Query,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        }
+                    },
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}{&filter*}", node.Children.First().Value.GetUrlTemplate());
+    }
+    [Fact]
+    public void GeneratesRequiredQueryParametersAndOptionalMixInOperation()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                              Parameters = {
+                                new OpenApiParameter {
+                                    Name = "id",
+                                    In = ParameterLocation.Path,
+                                    Required = true,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "filter",
+                                    In = ParameterLocation.Query,
+                                    Required = false,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "apikey",
+                                    In = ParameterLocation.Query,
+                                    Required = true,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                }
+                            },
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}{&filter*}", node.Children.First().Value.GetUrlTemplate());
+    }
+    [Fact]
+    public void GeneratesOnlyOptionalQueryParametersInPathItem()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Parameters = {
+                        new OpenApiParameter {
+                            Name = "id",
+                            In = ParameterLocation.Path,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "filter",
+                            In = ParameterLocation.Query,
+                            Required = false,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "apikey",
+                            In = ParameterLocation.Query,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        }
+                    },
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager{?apikey*,filter*}", node.Children.First().Value.GetUrlTemplate());
+    }
+    [Fact]
+    public void GeneratesOnlyOptionalQueryParametersInOperation()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                              Parameters = {
+                                new OpenApiParameter {
+                                    Name = "id",
+                                    In = ParameterLocation.Path,
+                                    Required = true,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "filter",
+                                    In = ParameterLocation.Query,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "apikey",
+                                    In = ParameterLocation.Query,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                }
+                            },
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager{?apikey*,filter*}", node.Children.First().Value.GetUrlTemplate());
+    }
+    [Fact]
+    public void GeneratesOnlyRequiredQueryParametersInPathItem()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Parameters = {
+                        new OpenApiParameter {
+                            Name = "id",
+                            In = ParameterLocation.Path,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "filter",
+                            In = ParameterLocation.Query,
+                            Required = true,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        },
+                        new OpenApiParameter {
+                            Name = "apikey",
+                            Required = true,
+                            In = ParameterLocation.Query,
+                            Schema = new OpenApiSchema {
+                                Type = "string"
+                            }
+                        }
+                    },
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}&filter={filter}", node.Children.First().Value.GetUrlTemplate());
+    }
+    [Fact]
+    public void GeneratesOnlyRequiredQueryParametersInOperation()
+    {
+        var doc = new OpenApiDocument
+        {
+            Paths = [],
+        };
+        doc.Paths.Add("users\\{id}\\manager", new()
+        {
+            Operations = new Dictionary<OperationType, OpenApiOperation> {
+                { OperationType.Get, new() {
+                              Parameters = {
+                                new OpenApiParameter {
+                                    Name = "id",
+                                    In = ParameterLocation.Path,
+                                    Required = true,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "filter",
+                                    Required = true,
+                                    In = ParameterLocation.Query,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                },
+                                new OpenApiParameter {
+                                    Name = "apikey",
+                                    Required = true,
+                                    In = ParameterLocation.Query,
+                                    Schema = new OpenApiSchema {
+                                        Type = "string"
+                                    }
+                                }
+                            },
+                    }
+                },
+            }
+        });
+        var node = OpenApiUrlTreeNode.Create(doc, Label);
+        Assert.Equal("{+baseurl}/users/{id}/manager?apikey={apikey}&filter={filter}", node.Children.First().Value.GetUrlTemplate());
+    }
 
     [Fact]
     public void GetUrlTemplateCleansInvalidParameters()
     {
         var doc = new OpenApiDocument
         {
-            Paths = new(),
+            Paths = [],
         };
         doc.Paths.Add("{param-with-dashes}\\existing-segment", new()
         {
             Operations = new Dictionary<OperationType, OpenApiOperation> {
                 { OperationType.Get, new() {
-                        Parameters = new List<OpenApiParameter> {
+                        Parameters = [
                             new() {
                                 Name = "param-with-dashes",
                                 In = ParameterLocation.Path,
@@ -241,13 +502,13 @@ public void GetUrlTemplateCleansInvalidParameters()
                                 },
                                 Style = ParameterStyle.Simple,
                             }
-                        }
+                        ]
                     }
                 }
             }
         });
         var node = OpenApiUrlTreeNode.Create(doc, Label);
-        Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select,api%2Dversion,api%7Etopic,api%2Eencoding}", node.Children.First().Value.GetUrlTemplate());
+        Assert.Equal("{+baseurl}/{param%2Dwith%2Ddashes}/existing-segment{?%24select,api%2Dversion,api%2Eencoding,api%7Etopic}", node.Children.First().Value.GetUrlTemplate());
         // the query parameters will be decoded by a middleware at runtime before the request is executed
     }
     [InlineData("\\reviews\\search.json", "reviews.searchJson")]