Repository: microsoft/kiota (csharp)

Issue:
size reduction: deduplicate error mappings
We have an opportunity to reduce the size of the projected SDKs, especially in the context of Microsoft Graph.
For a lot of APIs, the schema for the error responses, is the same across the error codes:
- Microsoft Graph describes ODataError for both 4XX and 5XX
- A lot of APIs use the default mapping for errors

Right now we project both those cases to two entries (4XX and 5XX), while it works it's wasteful.
- In the case of default, we could map things to a single XXX entry,
- In the case where we have only 4XX and 5XX, and both codes map to the same schema, we could reduce that to a single XXX entry.

This will require updating the request adapter implementations first to account for that special status code across languages.

Pull request description:
fixes #4025
holding off until the associated http PRs are in

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3de19746f5..64a19db0e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,13 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 ### Changed
+
 - Fixed mantis for bitwise enums in Go. [#3936](https://github.com/microsoft/kiota/issues/3936)
 - Keyword in enum names for go should not be escaped. [#2877](https://github.com/microsoft/kiota/issues/2877)
 - Generator method code reduction in Python. [#3695](https://github.com/microsoft/kiota/issues/3695)
-
 - Fixed return doc comments for Go/Java/CSharp/TypeScript.
 - Fixed type names in doc comments and deprecation noticed across languages.
 - Added thrown exceptions in doc comments for Go/CSharp/Java/TypeScript. [#3811](https://github.com/microsoft/kiota/issues/3811)
+- Deduplicates 4XX and 5XX error mappings when they map to the same type to reduce emitted code. [#4025](https://github.com/microsoft/kiota/issues/4025)
 - 📢📢📢 Java generation is now stable! 🚀🚀🚀 special thanks to @andreaTP (Red Hat) for all the help.
 
 ## [1.11.1] - 2024-02-05
diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs
index a1cc5dc3c5..086a2cad14 100644
--- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs
+++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs
@@ -257,8 +257,23 @@ public CodeIndexer? OriginalIndexer
     /// Avoids regex operations
     /// </summary>
     public string SimpleName { get; set; } = string.Empty;
+    /// <summary>
+    /// Deduplicates 4XX and 5XX error mappings into a single XXX mapping if they are the same.
+    /// </summary>
+    public void DeduplicateErrorMappings()
+    {
+        if (!errorMappings.TryGetValue(ErrorMappingClientRange, out var clientError) || !errorMappings.TryGetValue(ErrorMappingServerRange, out var serverError)) return;
+        if (clientError == serverError && errorMappings.TryAdd(ErrorMappingAllRange, clientError))
+        {
+            errorMappings.TryRemove(ErrorMappingServerRange, out var _);
+            errorMappings.TryRemove(ErrorMappingClientRange, out var _);
+        }
+    }
+    internal const string ErrorMappingClientRange = "4XX";
+    internal const string ErrorMappingServerRange = "5XX";
+    internal const string ErrorMappingAllRange = "XXX";
 
-    private ConcurrentDictionary<string, CodeTypeBase> errorMappings = new();
+    private ConcurrentDictionary<string, CodeTypeBase> errorMappings = new(StringComparer.OrdinalIgnoreCase);
 
     /// <summary>
     /// Mapping of the error code and response types for this method.
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index c381bdd15d..7cf88b6781 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -1279,9 +1279,7 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
     private const string RequestBodyPlainTextContentType = "text/plain";
     private static readonly HashSet<string> noContentStatusCodes = new(StringComparer.OrdinalIgnoreCase) { "201", "202", "204", "205" };
     private static readonly HashSet<string> errorStatusCodes = new(Enumerable.Range(400, 599).Select(static x => x.ToString(CultureInfo.InvariantCulture))
-                                                                                 .Concat(new[] { FourXXError, FiveXXError }), StringComparer.OrdinalIgnoreCase);
-    private const string FourXXError = "4XX";
-    private const string FiveXXError = "5XX";
+                                                                                 .Concat([CodeMethod.ErrorMappingClientRange, CodeMethod.ErrorMappingServerRange]), StringComparer.OrdinalIgnoreCase);
     private void AddErrorMappingsForExecutorMethod(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeMethod executorMethod)
     {
         foreach (var response in operation.Responses.Where(x => errorStatusCodes.Contains(x.Key)))
@@ -1293,10 +1291,10 @@ private void AddErrorMappingsForExecutorMethod(OpenApiUrlTreeNode currentNode, O
         }
         if (operation.Responses.TryGetValue("default", out var defaultResponse) && defaultResponse.GetResponseSchema(config.StructuredMimeTypes) is { } errorSchema)
         {
-            if (!executorMethod.HasErrorMappingCode(FourXXError))
-                AddErrorMappingToExecutorMethod(currentNode, operation, executorMethod, errorSchema, defaultResponse, FourXXError);
-            if (!executorMethod.HasErrorMappingCode(FiveXXError))
-                AddErrorMappingToExecutorMethod(currentNode, operation, executorMethod, errorSchema, defaultResponse, FiveXXError);
+            if (!executorMethod.HasErrorMappingCode(CodeMethod.ErrorMappingClientRange))
+                AddErrorMappingToExecutorMethod(currentNode, operation, executorMethod, errorSchema, defaultResponse, CodeMethod.ErrorMappingClientRange);
+            if (!executorMethod.HasErrorMappingCode(CodeMethod.ErrorMappingServerRange))
+                AddErrorMappingToExecutorMethod(currentNode, operation, executorMethod, errorSchema, defaultResponse, CodeMethod.ErrorMappingServerRange);
         }
     }
     private void AddErrorMappingToExecutorMethod(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeMethod executorMethod, OpenApiSchema errorSchema, OpenApiResponse response, string errorCode)
diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs
index 3920ad5220..9ca8c9241e 100644
--- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs
+++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs
@@ -21,6 +21,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
                 () => new CodeType { Name = "string", IsNullable = false, IsExternal = true },
                 true
             );
+            DeduplicateErrorMappings(generatedCode);
             MoveRequestBuilderPropertiesToBaseType(generatedCode,
                 new CodeUsing
                 {
diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
index 7e142e8c48..30b1b21455 100644
--- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
+++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
@@ -1527,4 +1527,12 @@ internal static void AddPrimaryErrorMessage(CodeElement currentElement, string n
         }
         CrawlTree(currentElement, x => AddPrimaryErrorMessage(x, name, type, asProperty));
     }
+    protected static void DeduplicateErrorMappings(CodeElement codeElement)
+    {
+        if (codeElement is CodeMethod { Kind: CodeMethodKind.RequestExecutor } requestExecutorMethod)
+        {
+            requestExecutorMethod.DeduplicateErrorMappings();
+        }
+        CrawlTree(codeElement, DeduplicateErrorMappings);
+    }
 }
diff --git a/src/Kiota.Builder/Refiners/GoRefiner.cs b/src/Kiota.Builder/Refiners/GoRefiner.cs
index 0878379326..fcdd9bce1d 100644
--- a/src/Kiota.Builder/Refiners/GoRefiner.cs
+++ b/src/Kiota.Builder/Refiners/GoRefiner.cs
@@ -18,6 +18,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         return Task.Run(() =>
         {
             cancellationToken.ThrowIfCancellationRequested();
+            DeduplicateErrorMappings(generatedCode);
             MoveRequestBuilderPropertiesToBaseType(generatedCode,
                 new CodeUsing
                 {
diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs
index c6145fa812..d1237449bf 100644
--- a/src/Kiota.Builder/Refiners/JavaRefiner.cs
+++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs
@@ -19,6 +19,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         {
             cancellationToken.ThrowIfCancellationRequested();
             CorrectCommonNames(generatedCode);
+            DeduplicateErrorMappings(generatedCode);
             AddQueryParameterExtractorMethod(generatedCode);
             MoveRequestBuilderPropertiesToBaseType(generatedCode,
                 new CodeUsing
diff --git a/src/Kiota.Builder/Refiners/PhpRefiner.cs b/src/Kiota.Builder/Refiners/PhpRefiner.cs
index f8830ceeb0..e53a544de7 100644
--- a/src/Kiota.Builder/Refiners/PhpRefiner.cs
+++ b/src/Kiota.Builder/Refiners/PhpRefiner.cs
@@ -20,6 +20,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         return Task.Run(() =>
         {
             RemoveMethodByKind(generatedCode, CodeMethodKind.RawUrlConstructor);
+            DeduplicateErrorMappings(generatedCode);
             AddInnerClasses(generatedCode,
                 true,
                 string.Empty,
diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs
index 2640af6acb..8f1f553f88 100644
--- a/src/Kiota.Builder/Refiners/PythonRefiner.cs
+++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs
@@ -16,6 +16,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         return Task.Run(() =>
         {
             cancellationToken.ThrowIfCancellationRequested();
+            DeduplicateErrorMappings(generatedCode);
             ConvertUnionTypesToWrapper(generatedCode,
                 _configuration.UsesBackingStore,
                 static s => s,
diff --git a/src/Kiota.Builder/Refiners/RubyRefiner.cs b/src/Kiota.Builder/Refiners/RubyRefiner.cs
index 90e5864dd2..0c334b3579 100644
--- a/src/Kiota.Builder/Refiners/RubyRefiner.cs
+++ b/src/Kiota.Builder/Refiners/RubyRefiner.cs
@@ -19,6 +19,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         {
             cancellationToken.ThrowIfCancellationRequested();
             RemoveMethodByKind(generatedCode, CodeMethodKind.RawUrlConstructor);
+            DeduplicateErrorMappings(generatedCode);
             ReplaceIndexersByMethodsWithParameter(generatedCode,
                 false,
                 static x => $"by_{x.ToSnakeCase()}",
diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
index 6d921a56f1..11e0358d9b 100644
--- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
+++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
@@ -17,6 +17,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
         return Task.Run(() =>
         {
             cancellationToken.ThrowIfCancellationRequested();
+            DeduplicateErrorMappings(generatedCode);
             RemoveMethodByKind(generatedCode, CodeMethodKind.RawUrlConstructor, CodeMethodKind.RawUrlBuilder);
             ReplaceReservedNames(generatedCode, new TypeScriptReservedNamesProvider(), static x => $"{x}Escaped");
             ReplaceReservedExceptionPropertyNames(generatedCode, new TypeScriptExceptionsReservedNamesProvider(), static x => $"{x}Escaped");
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
index 09388ac8ef..325797b642 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
@@ -81,7 +81,7 @@ private static void WriteNavigationMetadataEntry(CodeNamespace parentNamespace,
             writer.WriteLine($"pathParametersMappings: [{string.Join(", ", pathParameters)}],");
     }
     private static string GetErrorMappingKey(string original) =>
-    original.Equals("4XX", StringComparison.OrdinalIgnoreCase) || original.Equals("5XX", StringComparison.OrdinalIgnoreCase) ?
+    original.Equals(CodeMethod.ErrorMappingClientRange, StringComparison.OrdinalIgnoreCase) || original.Equals(CodeMethod.ErrorMappingServerRange, StringComparison.OrdinalIgnoreCase) ?
         $"_{original.ToUpperInvariant()}" : // to avoid emitting strings that can't be minified
         original.ToUpperInvariant();
 


Test patch:
diff --git a/tests/Kiota.Builder.Tests/CodeDOM/CodeMethodTests.cs b/tests/Kiota.Builder.Tests/CodeDOM/CodeMethodTests.cs
index 35069311cb..3340be4eed 100644
--- a/tests/Kiota.Builder.Tests/CodeDOM/CodeMethodTests.cs
+++ b/tests/Kiota.Builder.Tests/CodeDOM/CodeMethodTests.cs
@@ -114,4 +114,54 @@ public void ParametersExtensionsReturnsValue()
         Assert.NotNull(method.Parameters.OfKind(CodeParameterKind.Custom));
         Assert.Null(method.Parameters.OfKind(CodeParameterKind.RequestBody));
     }
+    [Fact]
+    public void DeduplicatesErrorMappings()
+    {
+        var method = new CodeMethod
+        {
+            Name = "method1",
+            ReturnType = new CodeType
+            {
+                Name = "string"
+            }
+        };
+        var commonType = new CodeType { Name = "string" };
+        method.AddErrorMapping("4XX", commonType);
+        method.AddErrorMapping("5XX", commonType);
+        method.DeduplicateErrorMappings();
+        Assert.Single(method.ErrorMappings);
+    }
+    [Fact]
+    public void DoesNotDeduplicateErrorMappingsOnDifferentTypes()
+    {
+        var method = new CodeMethod
+        {
+            Name = "method1",
+            ReturnType = new CodeType
+            {
+                Name = "string"
+            }
+        };
+        method.AddErrorMapping("4XX", new CodeType { Name = "string" });
+        method.AddErrorMapping("5XX", new CodeType { Name = "string" });
+        method.DeduplicateErrorMappings();
+        Assert.Equal(2, method.ErrorMappings.Count());
+    }
+    [Fact]
+    public void DoesNotDeduplicatesErrorMappingsWithSpecificCodes()
+    {
+        var method = new CodeMethod
+        {
+            Name = "method1",
+            ReturnType = new CodeType
+            {
+                Name = "string"
+            }
+        };
+        var commonType = new CodeType { Name = "string" };
+        method.AddErrorMapping("404", commonType);
+        method.AddErrorMapping("5XX", commonType);
+        method.DeduplicateErrorMappings();
+        Assert.Equal(2, method.ErrorMappings.Count());
+    }
 }