From ab89cb7d26c035256415f35c5d4c395168aba32c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 19 May 2026 23:01:00 +0000
Subject: [PATCH 1/7] Initial plan
From 4aff5f40b81599d6e18dd28e8c08d8ca9773661f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 19 May 2026 23:13:30 +0000
Subject: [PATCH 2/7] fix autoentity name generation for whitespace in SQL
metadata provider
Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/2051de6a-3432-4bd6-901c-3a3d6a8cc114
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
.../MsSqlMetadataProvider.cs | 23 +++++++++++++++++++
.../UnitTests/SqlMetadataProviderUnitTests.cs | 13 +++++++++++
2 files changed, 36 insertions(+)
diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
index cdb54a2ac2..2495951f55 100644
--- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
@@ -4,6 +4,7 @@
using System.Data;
using System.Data.Common;
using System.Net;
+using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
@@ -333,6 +334,8 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
continue;
}
+ entityName = SanitizeGeneratedEntityName(entityName);
+
// Create the entity using the template settings and permissions from the autoentity configuration.
// Currently the source type is always Table for auto-generated entities from database objects.
Entity generatedEntity = new(
@@ -412,5 +415,25 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
return resultArray;
}
+
+ internal static string SanitizeGeneratedEntityName(string name)
+ {
+ StringBuilder sanitizedName = new(name.Length);
+ bool capitalizeNext = false;
+
+ foreach (char character in name)
+ {
+ if (char.IsWhiteSpace(character))
+ {
+ capitalizeNext = true;
+ continue;
+ }
+
+ sanitizedName.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
+ capitalizeNext = false;
+ }
+
+ return sanitizedName.ToString();
+ }
}
}
diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
index dd6ad7d27e..45c1ccecfc 100644
--- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
+++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
@@ -610,6 +610,19 @@ private static async Task SetupTestFixtureAndInferMetadata()
await _sqlMetadataProvider.InitializeAsync();
}
+ ///
+ /// Ensures autoentity-generated entity names are sanitized for whitespace.
+ ///
+ [DataTestMethod]
+ [DataRow("dbo_Order Items", "dbo_OrderItems")]
+ [DataRow("Order Items", "OrderItems")]
+ [DataRow("NoWhitespace", "NoWhitespace")]
+ public void SanitizeGeneratedEntityName_SanitizesWhitespace(string input, string expected)
+ {
+ string actual = MsSqlMetadataProvider.SanitizeGeneratedEntityName(input);
+ Assert.AreEqual(expected, actual);
+ }
+
///
/// Ensures that the query that returns the tables that will be generated
/// into entities from the autoentities configuration returns the expected result.
From 4dbae6ed3ad0b4ff21c6d15a9a6f2fc1b42385ba Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Tue, 19 May 2026 17:23:50 -0700
Subject: [PATCH 3/7] Add tests
---
.../MsSqlMetadataProvider.cs | 27 ++++----------
.../MetadataProviders/SqlMetadataProvider.cs | 25 +++++++++++++
src/Service.Tests/DatabaseSchema-MsSql.sql | 37 ++++++++++++-------
.../UnitTests/SqlMetadataProviderUnitTests.cs | 14 +------
4 files changed, 57 insertions(+), 46 deletions(-)
diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
index 2495951f55..3ca6e96de1 100644
--- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
@@ -334,6 +334,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
continue;
}
+ // Sanitize the entity name by ensuring all whitespace characters are removed.
entityName = SanitizeGeneratedEntityName(entityName);
// Create the entity using the template settings and permissions from the autoentity configuration.
@@ -389,6 +390,12 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
_runtimeConfigProvider.AddMergedEntitiesToConfig(entities);
}
+ ///
+ /// Queries the database for autoentities based on the provided autoentity definition.
+ ///
+ /// The name of the autoentity definition.
+ /// The autoentity definition containing patterns for inclusion, exclusion, and name.
+ /// A JsonArray containing the queried autoentities, or null if none are found.
public async Task QueryAutoentitiesAsync(string autoentityName, Autoentity autoentity)
{
string include = string.Join(",", autoentity.Patterns.Include);
@@ -415,25 +422,5 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
return resultArray;
}
-
- internal static string SanitizeGeneratedEntityName(string name)
- {
- StringBuilder sanitizedName = new(name.Length);
- bool capitalizeNext = false;
-
- foreach (char character in name)
- {
- if (char.IsWhiteSpace(character))
- {
- capitalizeNext = true;
- continue;
- }
-
- sanitizedName.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
- capitalizeNext = false;
- }
-
- return sanitizedName.ToString();
- }
}
}
diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
index 3a85ba823e..f879e9560d 100644
--- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
@@ -733,6 +733,31 @@ private void RemoveGeneratedAutoentities()
_runtimeConfigProvider.RemoveGeneratedAutoentitiesFromConfig();
}
+ ///
+ /// Sanitizes the generated entity name by removing whitespace and capitalizing the next character after whitespace.
+ ///
+ /// The entity name to be sanitized.
+ /// The sanitized entity name.
+ protected static string SanitizeGeneratedEntityName(string name)
+ {
+ StringBuilder sanitizedName = new(name.Length);
+ bool capitalizeNext = false;
+
+ foreach (char character in name)
+ {
+ if (char.IsWhiteSpace(character))
+ {
+ capitalizeNext = true;
+ continue;
+ }
+
+ sanitizedName.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
+ capitalizeNext = false;
+ }
+
+ return sanitizedName.ToString();
+ }
+
protected void PopulateDatabaseObjectForEntity(
Entity entity,
string entityName,
diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql
index 4e87394aee..cd95cab950 100644
--- a/src/Service.Tests/DatabaseSchema-MsSql.sql
+++ b/src/Service.Tests/DatabaseSchema-MsSql.sql
@@ -63,6 +63,7 @@ DROP TABLE IF EXISTS date_only_table;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS user_profiles;
DROP TABLE IF EXISTS default_books;
+DROP TABLE IF EXISTS [Order Items];
DROP SCHEMA IF EXISTS [foo];
DROP SCHEMA IF EXISTS [bar];
COMMIT;
@@ -321,21 +322,23 @@ CREATE TABLE mappedbookmarks
bkname nvarchar(50) NOT NULL
)
-create Table fte_data(
-id int IDENTITY(5001,1),
-u_id int DEFAULT 2,
-name varchar(50),
-position varchar(20),
-salary int default 20,
-PRIMARY KEY(id, u_id)
+create Table fte_data
+(
+ id int IDENTITY(5001,1),
+ u_id int DEFAULT 2,
+ name varchar(50),
+ position varchar(20),
+ salary int default 20,
+ PRIMARY KEY(id, u_id)
);
-create Table intern_data(
-id int,
-months int default 2 NOT NULL,
-name varchar(50),
-salary int default 15,
-PRIMARY KEY(id, months)
+create Table intern_data
+(
+ id int,
+ months int default 2 NOT NULL,
+ name varchar(50),
+ salary int default 15,
+ PRIMARY KEY(id, months)
);
create table books_sold
@@ -394,6 +397,11 @@ CREATE TABLE default_books(
title NVARCHAR(100)
);
+CREATE TABLE [Order Items](
+ id INT PRIMARY KEY,
+ productname VARCHAR(100),
+);
+
ALTER TABLE books
ADD CONSTRAINT book_publisher_fk
FOREIGN KEY (publisher_id)
@@ -826,3 +834,6 @@ INSERT INTO date_only_table( event_date, event_time, event_timestamp)
VALUES ('2023-01-01', '08:30:00', '2023-01-01 08:30:00'),
('2023-02-15', '12:45:00', '2023-02-15 12:45:00'),
('2023-03-30', '17:15:00', '2023-03-30 17:15:00');
+
+INSERT INTO [Order Items](id, productname)
+VALUES (1, 'Sample Product');
diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
index 45c1ccecfc..88d53adcda 100644
--- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
+++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
@@ -610,19 +610,6 @@ private static async Task SetupTestFixtureAndInferMetadata()
await _sqlMetadataProvider.InitializeAsync();
}
- ///
- /// Ensures autoentity-generated entity names are sanitized for whitespace.
- ///
- [DataTestMethod]
- [DataRow("dbo_Order Items", "dbo_OrderItems")]
- [DataRow("Order Items", "OrderItems")]
- [DataRow("NoWhitespace", "NoWhitespace")]
- public void SanitizeGeneratedEntityName_SanitizesWhitespace(string input, string expected)
- {
- string actual = MsSqlMetadataProvider.SanitizeGeneratedEntityName(input);
- Assert.AreEqual(expected, actual);
- }
-
///
/// Ensures that the query that returns the tables that will be generated
/// into entities from the autoentities configuration returns the expected result.
@@ -633,6 +620,7 @@ public void SanitizeGeneratedEntityName_SanitizesWhitespace(string input, string
[DataRow(new string[] { "dbo.%book%" }, new string[] { "dbo.%books%" }, "{schema}_{object}_exclude_books", new string[] { "book" }, "books")]
[DataRow(new string[] { "dbo.%book%", "dbo.%publish%" }, new string[] { }, "{object}", new string[] { "book", "publish" }, "")]
[DataRow(new string[] { }, new string[] { "dbo.%book%" }, "{object}s", new string[] { "" }, "book")]
+ [DataRow(new string[] { "dbo.Order Items" }, new string[] { }, "{schema}_{object}", new string[] { "OrderItems" }, "")]
public async Task CheckAutoentitiesQuery(string[] include, string[] exclude, string name, string[] includeObject, string excludeObject)
{
// Arrange
From f322f1f11e58ee74ddf08977316bf6eff33bc4b3 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Fri, 22 May 2026 15:23:28 -0700
Subject: [PATCH 4/7] Fix tests
---
.../Configuration/ConfigurationTests.cs | 37 +++++++++++++++----
.../UnitTests/SqlMetadataProviderUnitTests.cs | 1 -
2 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 203de98aef..69e3016ccc 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -5759,16 +5759,17 @@ public async Task TestAutoentitiesWithSameObjectDifferentSchemas()
}
///
- /// Ensures that autoentities are properly generated into in-memory entities when entities have non-default schemas.
+ /// Ensures that autoentities are properly generated into in-memory entities when entities have unusual elements such as non-default schemas or spaces in their names.
///
/// The pattern to include for autoentities
/// Boolean that indicates if the pattern is for the foo schema
///
[TestCategory(TestCategory.MSSQL)]
[DataTestMethod]
- [DataRow("foo.%", true, DisplayName = "Test Autoentities with foo schema")]
- [DataRow("bar.%", false, DisplayName = "Test Autoentities with bar schema")]
- public async Task TestAutoentitiesGeneratedWithDifferentSchemas(string includePattern, bool isPatternFoo)
+ [DataRow("foo.%", 0, DisplayName = "Test Autoentities with foo schema")]
+ [DataRow("bar.%", 1, DisplayName = "Test Autoentities with bar schema")]
+ [DataRow("dbo.Order Items", 2, DisplayName = "Test Autoentities with object with spaces")]
+ public async Task TestAutoentitiesGeneratedWithUnusualElements(string includePattern, int patternType)
{
// Arrange
Dictionary autoentityMap = new()
@@ -5826,11 +5827,33 @@ public async Task TestAutoentitiesGeneratedWithDifferentSchemas(string includePa
using (HttpClient client = server.CreateClient())
{
// Act
- string path = isPatternFoo ? "foo_magazines" : "bar_magazines";
+ string path;
+ string item;
+ string expectedResponseFragment;
+ switch (patternType)
+ {
+ case 0:
+ path = "foo_magazines";
+ item = "title";
+ expectedResponseFragment = @"""title"":""Vogue""";
+ break;
+ case 1:
+ path = "bar_magazines";
+ item = "comic_name";
+ expectedResponseFragment = @"""comic_name"":""NotVogue""";
+ break;
+ case 2:
+ path = "dbo_OrderItems";
+ item = "productname";
+ expectedResponseFragment = @"""productname"":""Sample Product""";
+ break;
+ default:
+ throw new ArgumentException("Invalid pattern type");
+ }
+
using HttpRequestMessage restRequest = new(HttpMethod.Get, $"/api/{path}");
using HttpResponseMessage restResponse = await client.SendAsync(restRequest);
- string item = isPatternFoo ? "title" : "comic_name";
string graphqlQuery = $@"
{{
{path} {{
@@ -5848,8 +5871,6 @@ public async Task TestAutoentitiesGeneratedWithDifferentSchemas(string includePa
HttpResponseMessage graphqlResponse = await client.SendAsync(graphqlRequest);
// Assert
- string expectedResponseFragment = isPatternFoo ? @"""title"":""Vogue""" : @"""comic_name"":""NotVogue""";
-
// Verify REST response
Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode, "REST request to auto-generated entity should succeed");
diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
index 88d53adcda..dd6ad7d27e 100644
--- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
+++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs
@@ -620,7 +620,6 @@ private static async Task SetupTestFixtureAndInferMetadata()
[DataRow(new string[] { "dbo.%book%" }, new string[] { "dbo.%books%" }, "{schema}_{object}_exclude_books", new string[] { "book" }, "books")]
[DataRow(new string[] { "dbo.%book%", "dbo.%publish%" }, new string[] { }, "{object}", new string[] { "book", "publish" }, "")]
[DataRow(new string[] { }, new string[] { "dbo.%book%" }, "{object}s", new string[] { "" }, "book")]
- [DataRow(new string[] { "dbo.Order Items" }, new string[] { }, "{schema}_{object}", new string[] { "OrderItems" }, "")]
public async Task CheckAutoentitiesQuery(string[] include, string[] exclude, string name, string[] includeObject, string excludeObject)
{
// Arrange
From 9d24e8ff976a96d67ce124f1542f5a2885096534 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Fri, 22 May 2026 15:26:01 -0700
Subject: [PATCH 5/7] Fix syntax error
---
src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
index 3ca6e96de1..fe4c4612db 100644
--- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
@@ -4,7 +4,6 @@
using System.Data;
using System.Data.Common;
using System.Net;
-using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
From 4701eabb3ae4c92a4797a9eb388059703ceb01a2 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Fri, 22 May 2026 15:54:47 -0700
Subject: [PATCH 6/7] Changes based on copilot comments
---
src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs | 2 +-
src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +-
src/Service.Tests/DatabaseSchema-MsSql.sql | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
index fe4c4612db..6438a7a663 100644
--- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
@@ -394,7 +394,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
///
/// The name of the autoentity definition.
/// The autoentity definition containing patterns for inclusion, exclusion, and name.
- /// A JsonArray containing the queried autoentities, or null if none are found.
+ /// A JsonArray containing the queried autoentities, or an empty array if none are found.
public async Task QueryAutoentitiesAsync(string autoentityName, Autoentity autoentity)
{
string include = string.Join(",", autoentity.Patterns.Include);
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 69e3016ccc..15342308fd 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -5762,7 +5762,7 @@ public async Task TestAutoentitiesWithSameObjectDifferentSchemas()
/// Ensures that autoentities are properly generated into in-memory entities when entities have unusual elements such as non-default schemas or spaces in their names.
///
/// The pattern to include for autoentities
- /// Boolean that indicates if the pattern is for the foo schema
+ /// Integer that indicates which input pattern is being used
///
[TestCategory(TestCategory.MSSQL)]
[DataTestMethod]
diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql
index cd95cab950..472cfac133 100644
--- a/src/Service.Tests/DatabaseSchema-MsSql.sql
+++ b/src/Service.Tests/DatabaseSchema-MsSql.sql
@@ -399,7 +399,7 @@ CREATE TABLE default_books(
CREATE TABLE [Order Items](
id INT PRIMARY KEY,
- productname VARCHAR(100),
+ productname VARCHAR(100)
);
ALTER TABLE books
From 615e98283a97b9fe721fad266d066958b653ee40 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 9 Jun 2026 18:57:20 +0000
Subject: [PATCH 7/7] Address review feedback: rename sanitizer, guard empty
names, improve collision message, split whitespace test
---
.../MsSqlMetadataProvider.cs | 28 ++++-
.../MetadataProviders/SqlMetadataProvider.cs | 16 +--
.../Configuration/ConfigurationTests.cs | 118 ++++++++++++++++--
3 files changed, 145 insertions(+), 17 deletions(-)
diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
index 6438a7a663..f5bdee7357 100644
--- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
@@ -333,8 +333,25 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
continue;
}
- // Sanitize the entity name by ensuring all whitespace characters are removed.
- entityName = SanitizeGeneratedEntityName(entityName);
+ // Remove whitespace from the entity name and camelCase-join words so the result is
+ // a valid identifier for REST paths and GraphQL singular/plural names.
+ string rawEntityName = entityName;
+ entityName = RemoveWhitespaceAndCamelCase(entityName);
+
+ if (string.IsNullOrEmpty(entityName))
+ {
+ _logger.LogError(
+ "Skipping autoentity generation: entity name '{rawEntityName}' for schema '{schemaName}' resolves to an empty string after whitespace removal for autoentities definition '{autoentityName}'.",
+ rawEntityName, schemaName, autoentityName);
+ continue;
+ }
+
+ if (rawEntityName != entityName)
+ {
+ _logger.LogDebug(
+ "Entity name '{rawEntityName}' was normalized to '{entityName}' by removing whitespace.",
+ rawEntityName, entityName);
+ }
// Create the entity using the template settings and permissions from the autoentity configuration.
// Currently the source type is always Table for auto-generated entities from database objects.
@@ -357,10 +374,15 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona
// Add the generated entity to the linking entities dictionary.
// This allows the entity to be processed later during metadata population.
+ // A collision can occur when two database objects produce the same entity name after
+ // whitespace removal (e.g. "Order Item" and "OrderItem" both yield "OrderItem").
if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddGeneratedAutoentityNameToDataSourceName(entityName, autoentityName))
{
+ string collisionMessage = rawEntityName != entityName
+ ? $"Entity '{entityName}' (normalized from '{rawEntityName}' in schema '{schemaName}') conflicts with autoentity pattern '{autoentityName}'. Use --patterns.exclude to skip it."
+ : $"Entity '{entityName}' conflicts with autoentity pattern '{autoentityName}'. Use --patterns.exclude to skip it.";
throw new DataApiBuilderException(
- message: $"Entity '{entityName}' conflicts with autoentity pattern '{autoentityName}'. Use --patterns.exclude to skip it.",
+ message: collisionMessage,
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}
diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
index 9db0b48827..572354a549 100644
--- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
@@ -734,13 +734,15 @@ private void RemoveGeneratedAutoentities()
}
///
- /// Sanitizes the generated entity name by removing whitespace and capitalizing the next character after whitespace.
+ /// Removes whitespace from the generated entity name and capitalizes the character
+ /// immediately following each removed whitespace (camelCase join).
+ /// For example, "Order Items" becomes "OrderItems" and "dbo_Order Items" becomes "dbo_OrderItems".
///
- /// The entity name to be sanitized.
- /// The sanitized entity name.
- protected static string SanitizeGeneratedEntityName(string name)
+ /// The entity name to process.
+ /// The entity name with whitespace removed and following characters capitalized.
+ protected static string RemoveWhitespaceAndCamelCase(string name)
{
- StringBuilder sanitizedName = new(name.Length);
+ StringBuilder result = new(name.Length);
bool capitalizeNext = false;
foreach (char character in name)
@@ -751,11 +753,11 @@ protected static string SanitizeGeneratedEntityName(string name)
continue;
}
- sanitizedName.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
+ result.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
capitalizeNext = false;
}
- return sanitizedName.ToString();
+ return result.ToString();
}
protected void PopulateDatabaseObjectForEntity(
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 15342308fd..fe4402f926 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -5759,7 +5759,7 @@ public async Task TestAutoentitiesWithSameObjectDifferentSchemas()
}
///
- /// Ensures that autoentities are properly generated into in-memory entities when entities have unusual elements such as non-default schemas or spaces in their names.
+ /// Ensures that autoentities are properly generated into in-memory entities when entities have unusual elements such as non-default schemas.
///
/// The pattern to include for autoentities
/// Integer that indicates which input pattern is being used
@@ -5768,7 +5768,6 @@ public async Task TestAutoentitiesWithSameObjectDifferentSchemas()
[DataTestMethod]
[DataRow("foo.%", 0, DisplayName = "Test Autoentities with foo schema")]
[DataRow("bar.%", 1, DisplayName = "Test Autoentities with bar schema")]
- [DataRow("dbo.Order Items", 2, DisplayName = "Test Autoentities with object with spaces")]
public async Task TestAutoentitiesGeneratedWithUnusualElements(string includePattern, int patternType)
{
// Arrange
@@ -5842,11 +5841,6 @@ public async Task TestAutoentitiesGeneratedWithUnusualElements(string includePat
item = "comic_name";
expectedResponseFragment = @"""comic_name"":""NotVogue""";
break;
- case 2:
- path = "dbo_OrderItems";
- item = "productname";
- expectedResponseFragment = @"""productname"":""Sample Product""";
- break;
default:
throw new ArgumentException("Invalid pattern type");
}
@@ -5888,6 +5882,116 @@ public async Task TestAutoentitiesGeneratedWithUnusualElements(string includePat
}
}
+ ///
+ /// Ensures that autoentities are generated with valid names when the SQL object name contains spaces.
+ /// Whitespace is removed and the following character is capitalized (camelCase join), so that the
+ /// resulting entity name is a valid REST path segment and GraphQL type name.
+ /// For example, "dbo.[Order Items]" with the default pattern "{schema}_{object}" produces the
+ /// entity name "dbo_OrderItems" — not "dbo_Order Items".
+ ///
+ [TestCategory(TestCategory.MSSQL)]
+ [TestMethod]
+ public async Task TestAutoentitiesGeneratedWithSpacesInObjectName()
+ {
+ // Arrange
+ const string EXPECTED_ENTITY_NAME = "dbo_OrderItems";
+ const string EXPECTED_ITEM_FIELD = "productname";
+ const string EXPECTED_RESPONSE_FRAGMENT = @"""productname"":""Sample Product""";
+
+ Dictionary autoentityMap = new()
+ {
+ {
+ "SpacedObjectAutoEntity", new Autoentity(
+ Patterns: new AutoentityPatterns(
+ Include: new[] { "dbo.Order Items" },
+ Exclude: null,
+ Name: null
+ ),
+ Template: new AutoentityTemplate(
+ Rest: new EntityRestOptions(Enabled: true),
+ GraphQL: new EntityGraphQLOptions(
+ Singular: string.Empty,
+ Plural: string.Empty,
+ Enabled: true
+ ),
+ Health: null,
+ Cache: null
+ ),
+ Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }
+ )
+ }
+ };
+
+ DataSource dataSource = new(DatabaseType.MSSQL,
+ GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null);
+
+ RuntimeConfig configuration = new(
+ Schema: "TestAutoentitiesSpacesSchema",
+ DataSource: dataSource,
+ Runtime: new(
+ Rest: new(Enabled: true),
+ GraphQL: new(Enabled: true),
+ Mcp: new(Enabled: false),
+ Host: new(
+ Cors: null,
+ Authentication: new Config.ObjectModel.AuthenticationOptions(
+ Provider: nameof(EasyAuthType.StaticWebApps),
+ Jwt: null
+ )
+ )
+ ),
+ Entities: new(new Dictionary()),
+ Autoentities: new RuntimeAutoentities(autoentityMap)
+ );
+
+ File.WriteAllText(CUSTOM_CONFIG_FILENAME, configuration.ToJson());
+
+ string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" };
+ using (TestServer server = new(Program.CreateWebHostBuilder(args)))
+ using (HttpClient client = server.CreateClient())
+ {
+ // Assert that the sanitized entity name "dbo_OrderItems" is reachable via REST,
+ // explicitly confirming the generated name is EXPECTED_ENTITY_NAME and not "dbo_Order Items".
+ using HttpRequestMessage restRequest = new(HttpMethod.Get, $"/api/{EXPECTED_ENTITY_NAME}");
+ using HttpResponseMessage restResponse = await client.SendAsync(restRequest);
+ Assert.AreEqual(
+ HttpStatusCode.OK,
+ restResponse.StatusCode,
+ $"REST path '/api/{EXPECTED_ENTITY_NAME}' should exist; the entity name must be sanitized from 'dbo_Order Items' to '{EXPECTED_ENTITY_NAME}'.");
+
+ string restResponseBody = await restResponse.Content.ReadAsStringAsync();
+ Assert.IsTrue(!string.IsNullOrEmpty(restResponseBody), "REST response should contain data");
+ Assert.IsTrue(restResponseBody.Contains(EXPECTED_RESPONSE_FRAGMENT));
+
+ // Also verify via GraphQL using the sanitized name as the query root field.
+ string graphqlQuery = $@"
+ {{
+ {EXPECTED_ENTITY_NAME} {{
+ items {{
+ {EXPECTED_ITEM_FIELD}
+ }}
+ }}
+ }}";
+
+ object graphqlPayload = new { query = graphqlQuery };
+ HttpRequestMessage graphqlRequest = new(HttpMethod.Post, "/graphql")
+ {
+ Content = JsonContent.Create(graphqlPayload)
+ };
+ HttpResponseMessage graphqlResponse = await client.SendAsync(graphqlRequest);
+
+ Assert.AreEqual(
+ HttpStatusCode.OK,
+ graphqlResponse.StatusCode,
+ $"GraphQL query for '{EXPECTED_ENTITY_NAME}' should succeed with the sanitized entity name.");
+
+ string graphqlResponseBody = await graphqlResponse.Content.ReadAsStringAsync();
+ Assert.IsTrue(!string.IsNullOrEmpty(graphqlResponseBody), "GraphQL response should contain data");
+ Assert.IsFalse(graphqlResponseBody.Contains("errors"), "GraphQL response should not contain errors");
+ Assert.IsTrue(graphqlResponseBody.Contains(EXPECTED_RESPONSE_FRAGMENT));
+ }
+ }
+
///
/// Tests that DAB fails if the entities generated from autoentities property
/// do not contain unique parameters such as rest path, graphql singular/plural names,