From 65cacb3fa2a0674ac04fa90a4c825d61d2f9f00e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 21:09:05 +0000
Subject: [PATCH 1/2] Initial plan
From 7803d3df485e525a9de7f19f0e37e1bb977e21c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 21:25:08 +0000
Subject: [PATCH 2/2] Improve GraphQL naming conflict error messages with
entity and operation details
---
.../Configurations/RuntimeConfigValidator.cs | 81 ++++++++++++++-----
.../UnitTests/ConfigValidationUnitTests.cs | 64 ++++++++++++---
2 files changed, 115 insertions(+), 30 deletions(-)
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index 1c9f8b9ecd..334c46be00 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -907,7 +907,8 @@ public void ValidateDatabaseType(
///
public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType databaseType, RuntimeEntities entityCollection)
{
- HashSet graphQLOperationNames = new();
+ // Maps each generated GraphQL operation name to the entity name that registered it.
+ Dictionary graphQLOperationOwners = new();
foreach ((string entityName, Entity entity) in entityCollection)
{
@@ -916,16 +917,13 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
continue;
}
- bool containsDuplicateOperationNames = false;
+ List currentEntityOperations = new();
+
if (entity.Source.Type is EntitySourceType.StoredProcedure)
{
// For Stored Procedures a single query/mutation is generated.
string storedProcedureQueryName = GraphQLNaming.GenerateStoredProcedureGraphQLFieldName(entityName, entity);
-
- if (!graphQLOperationNames.Add(storedProcedureQueryName))
- {
- containsDuplicateOperationNames = true;
- }
+ currentEntityOperations.Add(storedProcedureQueryName);
}
else
{
@@ -938,29 +936,70 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
string listQueryName = GraphQLNaming.GenerateListQueryName(entityName, entity);
// Mutations names for the exposed entities are determined.
- string createMutationName = $"create{GraphQLNaming.GetDefinedSingularName(entityName, entity)}";
- string updateMutationName = $"update{GraphQLNaming.GetDefinedSingularName(entityName, entity)}";
- string deleteMutationName = $"delete{GraphQLNaming.GetDefinedSingularName(entityName, entity)}";
- string patchMutationName = $"patch{GraphQLNaming.GetDefinedSingularName(entityName, entity)}";
-
- if (!graphQLOperationNames.Add(pkQueryName)
- || !graphQLOperationNames.Add(listQueryName)
- || !graphQLOperationNames.Add(createMutationName)
- || !graphQLOperationNames.Add(updateMutationName)
- || !graphQLOperationNames.Add(deleteMutationName)
- || ((databaseType is DatabaseType.CosmosDB_NoSQL) && !graphQLOperationNames.Add(patchMutationName)))
+ string singularName = GraphQLNaming.GetDefinedSingularName(entityName, entity);
+ string createMutationName = $"create{singularName}";
+ string updateMutationName = $"update{singularName}";
+ string deleteMutationName = $"delete{singularName}";
+ string patchMutationName = $"patch{singularName}";
+
+ currentEntityOperations.Add(pkQueryName);
+ currentEntityOperations.Add(listQueryName);
+ currentEntityOperations.Add(createMutationName);
+ currentEntityOperations.Add(updateMutationName);
+ currentEntityOperations.Add(deleteMutationName);
+
+ if (databaseType is DatabaseType.CosmosDB_NoSQL)
{
- containsDuplicateOperationNames = true;
+ currentEntityOperations.Add(patchMutationName);
}
}
- if (containsDuplicateOperationNames)
+ // Identify all operations that are already registered by a previously processed entity.
+ List conflictingOperations = currentEntityOperations
+ .Where(op => graphQLOperationOwners.ContainsKey(op))
+ .ToList();
+
+ if (conflictingOperations.Count > 0)
{
+ // Identify which previously registered entities own the conflicting operations.
+ IEnumerable conflictingEntityNames = conflictingOperations
+ .Select(op => graphQLOperationOwners[op])
+ .Distinct();
+
+ string conflictingEntitiesStr = string.Join(
+ $"{Environment.NewLine} ",
+ conflictingEntityNames.Append(entityName));
+
+ string conflictingOpsStr = string.Join(
+ $"{Environment.NewLine} ",
+ conflictingOperations);
+
+ string message = $"GraphQL naming conflict detected.{Environment.NewLine}"
+ + $"{Environment.NewLine}Entities:{Environment.NewLine} {conflictingEntitiesStr}"
+ + $"{Environment.NewLine}{Environment.NewLine}The above entities generate conflicting GraphQL operation names:{Environment.NewLine} {conflictingOpsStr}";
+
+ if (entity.Source.Type is not EntitySourceType.StoredProcedure)
+ {
+ string singularName = GraphQLNaming.GetDefinedSingularName(entityName, entity);
+ string pluralName = GraphQLNaming.GetDefinedPluralName(entityName, entity);
+ message += $"{Environment.NewLine}{Environment.NewLine}GraphQL type names in conflict:{Environment.NewLine} Singular type: {singularName}{Environment.NewLine} Plural type: {pluralName}";
+ }
+
+ message += $"{Environment.NewLine}{Environment.NewLine}Configure distinct GraphQL singular and plural names for one of the entities to resolve this conflict.";
+
HandleOrRecordException(new DataApiBuilderException(
- message: $"Entity {entityName} generates queries/mutation that already exist",
+ message: message,
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
+ else
+ {
+ // No conflicts; register all operations for this entity.
+ foreach (string op in currentEntityOperations)
+ {
+ graphQLOperationOwners[op] = entityName;
+ }
+ }
}
}
diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
index 178115c86c..6096dd7835 100644
--- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
+++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
@@ -1259,7 +1259,7 @@ public void ValidateEntitiesWithGraphQLExposedGenerateDuplicateQueries(DatabaseT
{ "book", book },
{ "Book", bookWithUpperCase }
};
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "Book", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "Book", databaseType, conflictingEntityName: "book");
}
///
@@ -1302,7 +1302,7 @@ public void ValidateStoredProcedureAndTableGeneratedDuplicateQueries(DatabaseTyp
{ "executeBook", bookTable },
{ "Book_by_pk", bookByPkStoredProcedure }
};
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "executeBook", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "executeBook", databaseType, conflictingEntityName: "Book_by_pk");
}
///
@@ -1346,7 +1346,7 @@ public void ValidateStoredProcedureAndTableGeneratedDuplicateMutation(DatabaseTy
{ "ExecuteBooks", bookTable },
{ "AddBook", addBookStoredProcedure }
};
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "ExecuteBooks", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "ExecuteBooks", databaseType, conflictingEntityName: "AddBook");
}
///
@@ -1384,7 +1384,7 @@ public void ValidateEntitiesWithNameCollisionInGraphQLTypeGenerateDuplicateQueri
{ "book", book },
{ "book_alt", book_alt }
};
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, conflictingEntityName: "book");
}
///
@@ -1427,7 +1427,7 @@ public void ValidateEntitiesWithCollisionsInSingularPluralNamesGenerateDuplicate
{ "book", book },
{ "book_alt", book_alt }
};
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, conflictingEntityName: "book");
}
///
@@ -1465,7 +1465,45 @@ public void ValidateEntitiesWithNameCollisionInSingularPluralTypeGeneratesDuplic
entityCollection.Add("book_alt", book_alt);
entityCollection.Add("book", book);
- ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, conflictingEntityName: "book");
+ }
+
+ ///
+ /// Validates that a detailed error is thrown when autoentities includes objects whose names
+ /// differ only by singular/plural form (e.g. dbo.Category and dbo.Categories), causing
+ /// DAB to generate conflicting GraphQL type and operation names.
+ ///
+ /// "dbo_Category" entity → singular: Category, plural: Categories
+ /// "dbo_Categories" entity → singular: Category, plural: Categories (after pluralization)
+ ///
+ /// Both entities generate the same pk query, list query, and mutation names.
+ ///
+ [TestMethod]
+ [DataRow(DatabaseType.MSSQL)] // Relational Database
+ [DataRow(DatabaseType.CosmosDB_NoSQL)] // Non Relational Database
+ public void ValidateAutoEntitiesWithSingularPluralNameCollisionGenerateDuplicateQueries(DatabaseType databaseType)
+ {
+ // Entity Name: dbo_Category
+ // Singular: Category (from entity name processed by autoentities)
+ // Plural: Categories (pluralized from singular)
+ Entity categoryEntity = GraphQLTestHelpers.GenerateEntityWithSingularPlural("Category", "Categories");
+
+ // Entity Name: dbo_Categories
+ // Singular: Category (after singularization by autoentities)
+ // Plural: Categories
+ Entity categoriesEntity = GraphQLTestHelpers.GenerateEntityWithSingularPlural("Category", "Categories");
+
+ SortedDictionary entityCollection = new()
+ {
+ { "dbo_Categories", categoriesEntity },
+ { "dbo_Category", categoryEntity }
+ };
+
+ ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(
+ entityCollection,
+ "dbo_Category",
+ databaseType,
+ conflictingEntityName: "dbo_Categories");
}
///
@@ -1612,14 +1650,22 @@ public void TestGlobalRouteValidation(string graphQLConfiguredPath, string restC
/// queries with the same name.
///
/// Entity definitions
- /// Entity name to construct the expected exception message
- private static void ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(SortedDictionary entityCollection, string entityName, DatabaseType databaseType)
+ /// The entity name expected to appear in the conflict message as the conflicting entity.
+ /// Database type used during validation.
+ /// The other entity name expected to appear in the conflict message.
+ private static void ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(
+ SortedDictionary entityCollection,
+ string entityName,
+ DatabaseType databaseType,
+ string conflictingEntityName)
{
RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
DataApiBuilderException dabException = Assert.ThrowsException(
action: () => configValidator.ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(databaseType, new(entityCollection)));
- Assert.AreEqual(expected: $"Entity {entityName} generates queries/mutation that already exist", actual: dabException.Message);
+ StringAssert.StartsWith(dabException.Message, "GraphQL naming conflict detected.");
+ StringAssert.Contains(dabException.Message, entityName);
+ StringAssert.Contains(dabException.Message, conflictingEntityName);
Assert.AreEqual(expected: HttpStatusCode.ServiceUnavailable, actual: dabException.StatusCode);
Assert.AreEqual(expected: DataApiBuilderException.SubStatusCodes.ConfigValidationError, actual: dabException.SubStatusCode);
}