From 2d3defce9e3fc7a242bbf9f774644a8e326150c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:54:10 +0000 Subject: [PATCH 1/4] Initial plan From aafb94660dc0a2dc514f3783aa7b3d78d74c8358 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:16:44 +0000 Subject: [PATCH 2/4] Fix duplicate OpenAPI tags by reusing global tag instances Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../Services/OpenAPI/OpenApiDocumentor.cs | 47 +++++++++++-------- .../StoredProcedureGeneration.cs | 37 +++++++++++++++ 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 87fb96bc32..cec2f12571 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -139,16 +139,22 @@ public void CreateDocument(bool doOverrideExistingDocument = false) }; // Collect all entity tags and their descriptions for the top-level tags array - List globalTags = new(); + // Store tags in a dictionary to ensure we can reuse the same tag instances in BuildPaths + Dictionary globalTagsDict = new(); foreach (KeyValuePair kvp in runtimeConfig.Entities) { Entity entity = kvp.Value; string restPath = entity.Rest?.Path ?? kvp.Key; - globalTags.Add(new OpenApiTag + + // Only add the tag if it hasn't been added yet (handles entities with the same REST path) + if (!globalTagsDict.ContainsKey(restPath)) { - Name = restPath, - Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description - }); + globalTagsDict[restPath] = new OpenApiTag + { + Name = restPath, + Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description + }; + } } OpenApiDocument doc = new() @@ -162,9 +168,9 @@ public void CreateDocument(bool doOverrideExistingDocument = false) { new() { Url = url } }, - Paths = BuildPaths(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName), + Paths = BuildPaths(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, globalTagsDict), Components = components, - Tags = globalTags + Tags = globalTagsDict.Values.ToList() }; _openApiDocument = doc; } @@ -193,7 +199,7 @@ public void CreateDocument(bool doOverrideExistingDocument = false) /// "/EntityName" /// /// All possible paths in the DAB engine's REST API endpoint. - private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName) + private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName, Dictionary globalTags) { OpenApiPaths pathsCollection = new(); @@ -227,19 +233,22 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour continue; } - // Set the tag's Description property to the entity's semantic description if present. - OpenApiTag openApiTag = new() + // Reuse the existing tag from the global tags dictionary instead of creating a new one + // This ensures Swagger UI displays only one group per entity + List tags = new(); + if (globalTags.TryGetValue(entityRestPath, out OpenApiTag? existingTag)) { - Name = entityRestPath, - Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description - }; - - // The OpenApiTag will categorize all paths created using the entity's name or overridden REST path value. - // The tag categorization will instruct OpenAPI document visualization tooling to display all generated paths together. - List tags = new() + tags.Add(existingTag); + } + else { - openApiTag - }; + // Fallback: create a new tag if not found in global tags (should not happen in normal flow) + tags.Add(new OpenApiTag + { + Name = entityRestPath, + Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description + }); + } Dictionary configuredRestOperations = GetConfiguredRestOperations(entity, dbObject); diff --git a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs index ffd5aaadde..c1e42556a4 100644 --- a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs +++ b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs @@ -149,6 +149,43 @@ public void OpenApiDocumentor_TagsIncludeEntityDescription() $"Expected tag for '{entityName}' with description '{expectedDescription}' not found."); } + /// + /// Integration test validating that there are no duplicate tags in the OpenAPI document. + /// This test ensures that tags created in CreateDocument are reused in BuildPaths, + /// preventing Swagger UI from showing duplicate entity groups. + /// + [TestMethod] + public void OpenApiDocumentor_NosDuplicateTags() + { + // Act: Get the tags from the OpenAPI document + IList tags = _openApiDocument.Tags; + + // Get all tag names + var tagNames = tags.Select(t => t.Name).ToList(); + + // Get distinct tag names + var distinctTagNames = tagNames.Distinct().ToList(); + + // Assert: The number of tags should equal the number of distinct tag names (no duplicates) + Assert.AreEqual(distinctTagNames.Count, tagNames.Count, + $"Duplicate tags found in OpenAPI document. Tags: {string.Join(", ", tagNames)}"); + + // Additionally, verify that each operation references tags that are in the global tags list + foreach (var path in _openApiDocument.Paths) + { + foreach (var operation in path.Value.Operations) + { + foreach (var operationTag in operation.Value.Tags) + { + // Verify that the operation's tag is the same instance as one in the global tags + bool foundMatchingTag = tags.Any(globalTag => ReferenceEquals(globalTag, operationTag)); + Assert.IsTrue(foundMatchingTag, + $"Operation tag '{operationTag.Name}' at path '{path.Key}' is not the same instance as the global tag"); + } + } + } + } + /// /// Validates that the provided OpenApiReference object has the expected schema reference id /// and that that id is present in the list of component schema in the OpenApi document. From db2d784d576a88dc8635dbeb64ae8e63bc5378b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:17:55 +0000 Subject: [PATCH 3/4] Add missing System.Linq using statement Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index cec2f12571..46c7e456f5 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Net; using System.Net.Mime; using System.Text; From 9d7ece77b47516d1fbcb3c50ea54021eb26d4ce2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:18:53 +0000 Subject: [PATCH 4/4] Fix typo in test method name Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../OpenApiDocumentor/StoredProcedureGeneration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs index c1e42556a4..18f696430d 100644 --- a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs +++ b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs @@ -155,7 +155,7 @@ public void OpenApiDocumentor_TagsIncludeEntityDescription() /// preventing Swagger UI from showing duplicate entity groups. /// [TestMethod] - public void OpenApiDocumentor_NosDuplicateTags() + public void OpenApiDocumentor_NoDuplicateTags() { // Act: Get the tags from the OpenAPI document IList tags = _openApiDocument.Tags;