From 448161e448d69346150f1f9c0671afecde969321 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:20 +0000
Subject: [PATCH 01/11] Initial plan
From fefffeaa6ad38a206f71119433b45d01c070356c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 14 Jul 2026 21:22:12 +0000
Subject: [PATCH 02/11] Fix: propagate autoentity resolution counts to child
configs during validation
When using `data-source-files`, the metadata provider stores autoentity
resolution counts only on the root (merged) config. ValidateRootConfig
now copies those counts to each child config before per-child validation,
so child configs with only autoentities correctly pass the entity-presence
check instead of failing with "No entities found".
Adds two regression tests:
- TestChildWithDataSourceAndAutoentitiesResolvingEntitiesIsValid
- TestRootAndChildBothWithAutoentitiesResolvingEntitiesIsValid
---
src/Cli.Tests/ValidateConfigTests.cs | 66 +++++++++++++++++++
.../Configurations/RuntimeConfigValidator.cs | 12 ++++
2 files changed, 78 insertions(+)
diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs
index fe985f1a34..6e3f0fb1d5 100644
--- a/src/Cli.Tests/ValidateConfigTests.cs
+++ b/src/Cli.Tests/ValidateConfigTests.cs
@@ -883,6 +883,72 @@ public void TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning()
VerifyAutoentityZeroDiscoveredWarning(loggerMock, expectedFileNameInMessage: "child-db.json");
}
+ ///
+ /// Child config with only autoentities that resolve to >0 entities is valid.
+ /// Simulates the production code path where the metadata provider stores resolution
+ /// counts on the root (merged) config rather than on the child config directly.
+ /// This is the regression test for the bug where child-config autoentities caused
+ /// "No entities found" even when they were expanded successfully.
+ /// Covers child truth-table row C5 (DS=1, E=0, AE=1, resolved>0, counts on root).
+ ///
+ [TestMethod]
+ public void TestChildWithDataSourceAndAutoentitiesResolvingEntitiesIsValid()
+ {
+ // Child has no explicit entities; only autoentities.
+ RuntimeConfig childConfig = BuildTestConfig(
+ hasDataSource: true,
+ entities: new(),
+ autoentities: new() { { "ae1", BuildSimpleAutoentity() } });
+ childConfig.IsChildConfig = true;
+
+ RuntimeConfig rootConfig = BuildTestConfig(
+ hasDataSource: false, entities: new(),
+ dataSourceFiles: new DataSourceFiles(new[] { "child-db.json" }));
+ rootConfig.ChildConfigs.Add(("child-db.json", childConfig));
+
+ // Simulate production: metadata provider stores counts in the root config,
+ // NOT directly on the child config.
+ rootConfig.AutoentityResolutionCounts["ae1"] = 3;
+
+ RuntimeConfigValidator validator = BuildValidator(rootConfig);
+ validator.ValidateDataSourceAndEntityPresence(rootConfig);
+
+ Assert.AreEqual(0, validator.ConfigValidationExceptions.Count,
+ "Child config with autoentities that resolved entities should pass validation.");
+ }
+
+ ///
+ /// Both root and child configs have autoentities that resolve to >0 entities.
+ /// Resolution counts for both are stored only on the root config (as the metadata
+ /// provider does at runtime). Validation must pass for both configs.
+ ///
+ [TestMethod]
+ public void TestRootAndChildBothWithAutoentitiesResolvingEntitiesIsValid()
+ {
+ RuntimeConfig childConfig = BuildTestConfig(
+ hasDataSource: true,
+ entities: new(),
+ autoentities: new() { { "child-ae", BuildSimpleAutoentity() } });
+ childConfig.IsChildConfig = true;
+
+ RuntimeConfig rootConfig = BuildTestConfig(
+ hasDataSource: true,
+ entities: new(),
+ dataSourceFiles: new DataSourceFiles(new[] { "child-db.json" }),
+ autoentities: new() { { "root-ae", BuildSimpleAutoentity() } });
+ rootConfig.ChildConfigs.Add(("child-db.json", childConfig));
+
+ // Simulate production: metadata provider stores ALL counts in the root config.
+ rootConfig.AutoentityResolutionCounts["root-ae"] = 2;
+ rootConfig.AutoentityResolutionCounts["child-ae"] = 4;
+
+ RuntimeConfigValidator validator = BuildValidator(rootConfig);
+ validator.ValidateDataSourceAndEntityPresence(rootConfig);
+
+ Assert.AreEqual(0, validator.ConfigValidationExceptions.Count,
+ "Root and child configs both with autoentities that resolved entities should pass validation.");
+ }
+
///
/// Helper: verifies that the autoentity-discovered-zero warning was logged at least once,
/// optionally also checking that the formatted message contains a child config file name.
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index 1c9f8b9ecd..c8a6c6e2c9 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -739,6 +739,18 @@ private void ValidateRootConfig(RuntimeConfig runtimeConfig)
// Validate each child config independently.
foreach ((string fileName, RuntimeConfig childConfig) in runtimeConfig.ChildConfigs)
{
+ // The metadata provider stores autoentity resolution counts on the root (merged)
+ // config. Copy those counts to the child config so per-child validation can find
+ // them. Only copy if the child hasn't already been populated (e.g. in unit tests).
+ foreach (KeyValuePair ae in childConfig.Autoentities)
+ {
+ if (!childConfig.AutoentityResolutionCounts.ContainsKey(ae.Key)
+ && runtimeConfig.AutoentityResolutionCounts.TryGetValue(ae.Key, out int count))
+ {
+ childConfig.AutoentityResolutionCounts[ae.Key] = count;
+ }
+ }
+
ValidateNonRootConfig(childConfig, configName: fileName);
}
}
From bb1fce07970468618e4b05309ec478351a237b3c Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 22 Jul 2026 13:44:16 -0700
Subject: [PATCH 03/11] remove unecessary comments
---
src/Cli.Tests/ValidateConfigTests.cs | 5 +----
src/Core/Configurations/RuntimeConfigValidator.cs | 5 ++---
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs
index 6e3f0fb1d5..8bc87c6d66 100644
--- a/src/Cli.Tests/ValidateConfigTests.cs
+++ b/src/Cli.Tests/ValidateConfigTests.cs
@@ -884,12 +884,9 @@ public void TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning()
}
///
- /// Child config with only autoentities that resolve to >0 entities is valid.
+ /// Child config with only autoentities that resolve to greater than 0 entities is valid.
/// Simulates the production code path where the metadata provider stores resolution
/// counts on the root (merged) config rather than on the child config directly.
- /// This is the regression test for the bug where child-config autoentities caused
- /// "No entities found" even when they were expanded successfully.
- /// Covers child truth-table row C5 (DS=1, E=0, AE=1, resolved>0, counts on root).
///
[TestMethod]
public void TestChildWithDataSourceAndAutoentitiesResolvingEntitiesIsValid()
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index c8a6c6e2c9..d2527aba4f 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -739,9 +739,8 @@ private void ValidateRootConfig(RuntimeConfig runtimeConfig)
// Validate each child config independently.
foreach ((string fileName, RuntimeConfig childConfig) in runtimeConfig.ChildConfigs)
{
- // The metadata provider stores autoentity resolution counts on the root (merged)
- // config. Copy those counts to the child config so per-child validation can find
- // them. Only copy if the child hasn't already been populated (e.g. in unit tests).
+ // The metadata provider stores autoentity resolution counts on the root config.
+ // Copy those counts to the child config so per-child validation can find them
foreach (KeyValuePair ae in childConfig.Autoentities)
{
if (!childConfig.AutoentityResolutionCounts.ContainsKey(ae.Key)
From 00b98e7d7666095623577376c0c519a670f9339e Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Thu, 10 Sep 2026 13:40:50 -0700
Subject: [PATCH 04/11] Bug fix
---
src/Config/ObjectModel/RuntimeConfig.cs | 4 ++--
src/Core/Configurations/RuntimeConfigValidator.cs | 15 +++------------
2 files changed, 5 insertions(+), 14 deletions(-)
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index 21d2c23252..5f4b07dfd7 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -397,8 +397,8 @@ public RuntimeConfig(
_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
- allEntities = allEntities?.Concat(config.Entities.AsEnumerable());
- allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable());
+ allEntities = allEntities is null ? config.Entities.AsEnumerable() : allEntities.Concat(config.Entities.AsEnumerable());
+ allAutoentities = allAutoentities is null ? config.Autoentities.AsEnumerable() : allAutoentities.Concat(config.Autoentities.AsEnumerable());
}
catch (Exception e)
{
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index ba9c49a914..eb05a82c11 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -740,17 +740,6 @@ private void ValidateRootConfig(RuntimeConfig runtimeConfig)
// Validate each child config independently.
foreach ((string fileName, RuntimeConfig childConfig) in runtimeConfig.ChildConfigs)
{
- // The metadata provider stores autoentity resolution counts on the root config.
- // Copy those counts to the child config so per-child validation can find them
- foreach (KeyValuePair ae in childConfig.Autoentities)
- {
- if (!childConfig.AutoentityResolutionCounts.ContainsKey(ae.Key)
- && runtimeConfig.AutoentityResolutionCounts.TryGetValue(ae.Key, out int count))
- {
- childConfig.AutoentityResolutionCounts[ae.Key] = count;
- }
- }
-
ValidateNonRootConfig(childConfig, configName: fileName);
}
}
@@ -798,9 +787,11 @@ private void ValidateEntityPresence(RuntimeConfig config, string? configName)
if (autoentitiesPropertyExists)
{
+ RuntimeConfig rootConfig = _runtimeConfigProvider.GetConfig();
+
foreach (KeyValuePair autoentityDef in config.Autoentities)
{
- if (config.AutoentityResolutionCounts.TryGetValue(autoentityDef.Key, out int resolvedCount))
+ if (rootConfig.AutoentityResolutionCounts.TryGetValue(autoentityDef.Key, out int resolvedCount))
{
resolvedAutoentityCount += resolvedCount;
}
From 2b66f980e629b3ebd1d8d99a281a3f91423aae59 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Thu, 17 Sep 2026 10:43:18 -0700
Subject: [PATCH 05/11] Move test
---
.../Configuration/ConfigurationTests.cs | 115 ++++++++++++++++++
1 file changed, 115 insertions(+)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index cee78475d9..bd7e4a41f1 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -46,6 +46,7 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -6208,6 +6209,120 @@ public async Task ValidateAutoentitiesConfiguration()
"Unexpected autoentity-related validation error.");
}
+ ///
+ /// End-to-end regression test that creates actual root and child JSON files, loads them through
+ /// , runs metadata initialization and validation via
+ /// , and asserts the PER-FILE result.
+ ///
+ /// The root references the child through data-source-files. The root's own autoentity
+ /// resolves ZERO entities (its pattern matches no table) while the child's autoentities for MSSQL
+ /// resolve real tables (dbo.books). After the child's autoentities are merged into the root, the
+ /// merged config has resolvable entities, so validation succeeds and no "No entities found"
+ /// presence error is produced.
+ /// Requires a running MSSQL instance reachable via the standard MSSQL test connection string.
+ ///
+ [TestMethod]
+ [TestCategory(TestCategory.MSSQL)]
+ public async Task TestValidate_MultiConfigRootResolvingZero_ProducesRootScopedErrorAndValidChild()
+ {
+ // Root autoentity matches nothing (resolves 0); child matches dbo.books (resolves).
+ (string rootConfigPath, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) =
+ ArrangeMultiConfigForMsSql();
+
+ // Point the loader at the root config and load through the provider so the child is merged.
+ loader.UpdateConfigFilePath(rootConfigPath);
+ RuntimeConfigProvider provider = new(loader);
+ Assert.IsTrue(provider.TryGetConfig(out RuntimeConfig mergedRoot) && mergedRoot is not null,
+ "Root config with data-source-files should load and merge the child config.");
+ Assert.AreEqual(1, mergedRoot.ChildConfigs.Count, "The child config should have been merged in.");
+
+ ILoggerFactory loggerFactory = new LoggerFactory();
+ RuntimeConfigValidator validator = new(
+ provider,
+ fileSystem,
+ loggerFactory.CreateLogger(),
+ isValidateOnly: true);
+
+ // Runs metadata initialization (real autoentity resolution against MSSQL) + presence validation.
+ bool isValid = await validator.TryValidateConfig(rootConfigPath, loggerFactory);
+ Assert.IsTrue(isValid, "Validation should succeed");
+
+ List presenceErrors = validator.ConfigValidationExceptions
+ .Where(e => e.Message.Contains("No entities found"))
+ .ToList();
+ Assert.AreEqual(0, presenceErrors.Count,
+ "Expected no errors to be found");
+ }
+
+ ///
+ /// Helper: builds a real multi-config on disk for MSSQL directly through the
+ /// object model (the pattern used throughout these Service.Tests),
+ /// rather than the CLI generators which are not referenced by this test project:
+ /// - a CHILD config with its own MSSQL data source and autoentities matching dbo.books,
+ /// - a ROOT config with its own MSSQL data source, autoentities matching
+ /// , and data-source-files pointing at the child.
+ /// Returns the root config path plus a fresh loader and file system to drive validation.
+ ///
+ private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoader, IFileSystem FileSystem)
+ ArrangeMultiConfigForMsSql()
+ {
+ string connectionString = GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL);
+
+ // Use a real file system + temp directory so the RuntimeConfig constructor (which loads
+ // data-source-files through the real file system) can find and merge the child config.
+ IFileSystem fileSystem = new FileSystem();
+
+ // Child: own MSSQL data source + autoentities matching a real table (dbo.books).
+ RuntimeConfig childConfig = new(
+ Schema: "child-schema",
+ DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
+ Entities: new(new Dictionary()),
+ Autoentities: new(BuildAutoentityMap(definitionName: "child-filter", patternInclude: "dbo.books")),
+ Runtime: new(
+ Rest: new(),
+ GraphQL: new(),
+ Mcp: new(),
+ Host: new(null, null, HostMode.Development)));
+ File.WriteAllText("dab-child.json", childConfig.ToJson());
+
+ // Root: own MSSQL data source + autoentities (pattern controls whether it resolves) +
+ // data-source-files pointing at the child.
+ RuntimeConfig rootConfig = new(
+ Schema: "root-schema",
+ DataSource: null,
+ Entities: new(new Dictionary()),
+ Runtime: new(
+ Rest: new(),
+ GraphQL: new(),
+ Mcp: new(),
+ Host: new(null, null, HostMode.Development)),
+ DataSourceFiles: new DataSourceFiles(new[] { "dab-child.json" }));
+ File.WriteAllText("dab-root.json", rootConfig.ToJson());
+
+ return ("dab-root.json", new FileSystemRuntimeConfigLoader(fileSystem), fileSystem);
+ }
+
+ ///
+ /// Helper: builds an autoentity map containing a single definition whose include pattern
+ /// controls which tables it resolves against the target database.
+ ///
+ private static Dictionary BuildAutoentityMap(string definitionName, string patternInclude)
+ {
+ EntityAction entityAction = new(EntityActionOperation.Read, null, null);
+
+ Autoentity autoentity = new(
+ Patterns: new AutoentityPatterns(
+ Include: new[] { patternInclude },
+ Exclude: Array.Empty(),
+ Name: "{object}"),
+ Template: new AutoentityTemplate(
+ Rest: new(Enabled: true),
+ GraphQL: new(Enabled: true, Singular: string.Empty, Plural: string.Empty)),
+ Permissions: new EntityPermission[] { new("anonymous", new EntityAction[] { entityAction }) });
+
+ return new Dictionary { { definitionName, autoentity } };
+ }
+
///
/// Tests the behavior of GraphQL queries in non-hosted mode when the depth limit is explicitly set to -1 or null.
/// Setting the depth limit to -1 is intended to disable the depth limit check, allowing queries of any depth.
From c8405a17e1461404517c674378dfe83c6bc0e6ec Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Tue, 22 Sep 2026 15:10:23 -0700
Subject: [PATCH 06/11] Bug fixes
---
src/Config/ObjectModel/RuntimeConfig.cs | 21 +++++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index 5f4b07dfd7..fc5d9f6444 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -370,6 +370,10 @@ public RuntimeConfig(
{
IEnumerable>? allEntities = Entities?.AsEnumerable();
IEnumerable>? allAutoentities = Autoentities?.AsEnumerable();
+
+ HashSet alreadyReviewedEntityNames = new(this.Entities.Entities.Keys);
+ HashSet alreadyReviewedAutoentityNames = new(this.Autoentities.Autoentities.Keys);
+
// Iterate through all the datasource files and load the config.
IFileSystem fileSystem = new FileSystem();
// This loader is not used as a part of hot reload and therefore does not need a handler.
@@ -394,11 +398,24 @@ public RuntimeConfig(
// Store the child config reference for per-child validation.
ChildConfigs.Add((dataSourceFile, config));
+ // Skip datasource files that were already reviewed: if the child has content
+ // and every one of its entities and autoentities is already present, it was
+ // merged in a previous construction and must not be added again.
+ bool childHasContent = config.Entities.Entities.Count > 0 || config.Autoentities.Autoentities.Count > 0;
+ bool alreadyReviewed = childHasContent
+ && config.Entities.Entities.Keys.All(alreadyReviewedEntityNames.Contains)
+ && config.Autoentities.Autoentities.Keys.All(alreadyReviewedAutoentityNames.Contains);
+
+ /*if (alreadyReviewed)
+ {
+ continue;
+ }*/
+
_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
- allEntities = allEntities is null ? config.Entities.AsEnumerable() : allEntities.Concat(config.Entities.AsEnumerable());
- allAutoentities = allAutoentities is null ? config.Autoentities.AsEnumerable() : allAutoentities.Concat(config.Autoentities.AsEnumerable());
+ allEntities = allEntities?.Concat(config.Entities.AsEnumerable());
+ allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable());
}
catch (Exception e)
{
From af67c00a9c8b2fed3f3d8df1c3c7d778ecd19b2b Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 23 Sep 2026 17:38:08 -0700
Subject: [PATCH 07/11] Fix bug
---
src/Config/ObjectModel/RuntimeConfig.cs | 39 +++++++++----------
.../MetadataProviderFactory.cs | 16 +++++---
2 files changed, 29 insertions(+), 26 deletions(-)
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index fc5d9f6444..77d6e7348c 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -258,6 +258,8 @@ Runtime.GraphQL.FeatureFlags is not null &&
private Dictionary _entityPathNameToEntityName = new();
+ private Dictionary _dataSourceNameToConfigLoader = new();
+
///
/// List of all datasources.
///
@@ -305,6 +307,11 @@ public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName)
return _entityNameToDataSourceName.Remove(entityName);
}
+ public bool TryGetConfigLoaderFromDataSourceName(string dataSourceName, [NotNullWhen(true)] out FileSystemRuntimeConfigLoader? configLoader)
+ {
+ return _dataSourceNameToConfigLoader.TryGetValue(dataSourceName, out configLoader);
+ }
+
///
/// Constructor for runtimeConfig.
/// To be used when setting up from cli json scenario.
@@ -370,24 +377,26 @@ public RuntimeConfig(
{
IEnumerable>? allEntities = Entities?.AsEnumerable();
IEnumerable>? allAutoentities = Autoentities?.AsEnumerable();
-
- HashSet alreadyReviewedEntityNames = new(this.Entities.Entities.Keys);
- HashSet alreadyReviewedAutoentityNames = new(this.Autoentities.Autoentities.Keys);
-
// Iterate through all the datasource files and load the config.
IFileSystem fileSystem = new FileSystem();
- // This loader is not used as a part of hot reload and therefore does not need a handler.
- FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null);
// Pass the parent's AKV options so @akv() references in child configs can
// be resolved using the parent's Key Vault configuration.
// If a child config defines its own azure-key-vault section, TryParseConfig's
// ExtractAzureKeyVaultOptions will detect it and override these parent options.
- DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: this.AzureKeyVault, doReplaceEnvVar: true, doReplaceAkvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore);
+ DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: this.AzureKeyVault, doReplaceEnvVar: true, doReplaceAkvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore)
+ {
+ // Defer Application Name (telemetry) injection to the top-level load. A child config
+ // has no global runtime section and only its own entities; the root performs the
+ // injection once over the fully-merged config so each data source's pool reflects the
+ // global runtime and the complete entity set.
+ SkipApplicationNameInjection = true
+ };
foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
{
-
+ // This loader is not used as a part of hot reload and therefore does not need a handler.
+ FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null, baseConfigFilePath: dataSourceFile);
if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings))
{
try
@@ -398,19 +407,7 @@ public RuntimeConfig(
// Store the child config reference for per-child validation.
ChildConfigs.Add((dataSourceFile, config));
- // Skip datasource files that were already reviewed: if the child has content
- // and every one of its entities and autoentities is already present, it was
- // merged in a previous construction and must not be added again.
- bool childHasContent = config.Entities.Entities.Count > 0 || config.Autoentities.Autoentities.Count > 0;
- bool alreadyReviewed = childHasContent
- && config.Entities.Entities.Keys.All(alreadyReviewedEntityNames.Contains)
- && config.Autoentities.Autoentities.Keys.All(alreadyReviewedAutoentityNames.Contains);
-
- /*if (alreadyReviewed)
- {
- continue;
- }*/
-
+ _dataSourceNameToConfigLoader.TryAdd(config.DefaultDataSourceName, loader);
_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
diff --git a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs
index 6fe20969ed..2191f51171 100644
--- a/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs
+++ b/src/Core/Services/MetadataProviders/MetadataProviderFactory.cs
@@ -49,13 +49,19 @@ private void ConfigureMetadataProviders()
{
foreach ((string dataSourceName, DataSource dataSource) in _runtimeConfigProvider.GetConfig().GetDataSourceNamesToDataSourcesIterator())
{
+ RuntimeConfigProvider runtimeConfigProvider = _runtimeConfigProvider;
+ if (_runtimeConfigProvider.GetConfig().TryGetConfigLoaderFromDataSourceName(dataSourceName, out FileSystemRuntimeConfigLoader? configLoader))
+ {
+ runtimeConfigProvider = new RuntimeConfigProvider(configLoader);
+ }
+
ISqlMetadataProvider metadataProvider = dataSource.DatabaseType switch
{
- DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _fileSystem),
- DatabaseType.MSSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
- DatabaseType.DWSQL => new MsSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
- DatabaseType.PostgreSQL => new PostgreSqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
- DatabaseType.MySQL => new MySqlMetadataProvider(_runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
+ DatabaseType.CosmosDB_NoSQL => new CosmosSqlMetadataProvider(runtimeConfigProvider, _runtimeConfigValidator, _fileSystem),
+ DatabaseType.MSSQL => new MsSqlMetadataProvider(runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
+ DatabaseType.DWSQL => new MsSqlMetadataProvider(runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
+ DatabaseType.PostgreSQL => new PostgreSqlMetadataProvider(runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
+ DatabaseType.MySQL => new MySqlMetadataProvider(runtimeConfigProvider, _runtimeConfigValidator, _queryManagerFactory, _logger, dataSourceName, _isValidateOnly),
_ => throw new NotSupportedException(dataSource.DatabaseTypeNotSupportedMessage),
};
From de5515a192b3cbde2a94373dcf25168ff206fdf1 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 23 Sep 2026 17:59:14 -0700
Subject: [PATCH 08/11] Remove validator changes
---
src/Core/Configurations/RuntimeConfigValidator.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs
index eb05a82c11..9ccd734c08 100644
--- a/src/Core/Configurations/RuntimeConfigValidator.cs
+++ b/src/Core/Configurations/RuntimeConfigValidator.cs
@@ -787,11 +787,9 @@ private void ValidateEntityPresence(RuntimeConfig config, string? configName)
if (autoentitiesPropertyExists)
{
- RuntimeConfig rootConfig = _runtimeConfigProvider.GetConfig();
-
foreach (KeyValuePair autoentityDef in config.Autoentities)
{
- if (rootConfig.AutoentityResolutionCounts.TryGetValue(autoentityDef.Key, out int resolvedCount))
+ if (config.AutoentityResolutionCounts.TryGetValue(autoentityDef.Key, out int resolvedCount))
{
resolvedAutoentityCount += resolvedCount;
}
From 09112ad586956c4c9ea2d0bb1a48435467c16d0b Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Thu, 24 Sep 2026 13:53:38 -0700
Subject: [PATCH 09/11] Fix tests
---
src/Cli.Tests/ValidateConfigTests.cs | 63 -------------------
.../Configuration/ConfigurationTests.cs | 36 +++++------
2 files changed, 17 insertions(+), 82 deletions(-)
diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs
index 8bc87c6d66..fe985f1a34 100644
--- a/src/Cli.Tests/ValidateConfigTests.cs
+++ b/src/Cli.Tests/ValidateConfigTests.cs
@@ -883,69 +883,6 @@ public void TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning()
VerifyAutoentityZeroDiscoveredWarning(loggerMock, expectedFileNameInMessage: "child-db.json");
}
- ///
- /// Child config with only autoentities that resolve to greater than 0 entities is valid.
- /// Simulates the production code path where the metadata provider stores resolution
- /// counts on the root (merged) config rather than on the child config directly.
- ///
- [TestMethod]
- public void TestChildWithDataSourceAndAutoentitiesResolvingEntitiesIsValid()
- {
- // Child has no explicit entities; only autoentities.
- RuntimeConfig childConfig = BuildTestConfig(
- hasDataSource: true,
- entities: new(),
- autoentities: new() { { "ae1", BuildSimpleAutoentity() } });
- childConfig.IsChildConfig = true;
-
- RuntimeConfig rootConfig = BuildTestConfig(
- hasDataSource: false, entities: new(),
- dataSourceFiles: new DataSourceFiles(new[] { "child-db.json" }));
- rootConfig.ChildConfigs.Add(("child-db.json", childConfig));
-
- // Simulate production: metadata provider stores counts in the root config,
- // NOT directly on the child config.
- rootConfig.AutoentityResolutionCounts["ae1"] = 3;
-
- RuntimeConfigValidator validator = BuildValidator(rootConfig);
- validator.ValidateDataSourceAndEntityPresence(rootConfig);
-
- Assert.AreEqual(0, validator.ConfigValidationExceptions.Count,
- "Child config with autoentities that resolved entities should pass validation.");
- }
-
- ///
- /// Both root and child configs have autoentities that resolve to >0 entities.
- /// Resolution counts for both are stored only on the root config (as the metadata
- /// provider does at runtime). Validation must pass for both configs.
- ///
- [TestMethod]
- public void TestRootAndChildBothWithAutoentitiesResolvingEntitiesIsValid()
- {
- RuntimeConfig childConfig = BuildTestConfig(
- hasDataSource: true,
- entities: new(),
- autoentities: new() { { "child-ae", BuildSimpleAutoentity() } });
- childConfig.IsChildConfig = true;
-
- RuntimeConfig rootConfig = BuildTestConfig(
- hasDataSource: true,
- entities: new(),
- dataSourceFiles: new DataSourceFiles(new[] { "child-db.json" }),
- autoentities: new() { { "root-ae", BuildSimpleAutoentity() } });
- rootConfig.ChildConfigs.Add(("child-db.json", childConfig));
-
- // Simulate production: metadata provider stores ALL counts in the root config.
- rootConfig.AutoentityResolutionCounts["root-ae"] = 2;
- rootConfig.AutoentityResolutionCounts["child-ae"] = 4;
-
- RuntimeConfigValidator validator = BuildValidator(rootConfig);
- validator.ValidateDataSourceAndEntityPresence(rootConfig);
-
- Assert.AreEqual(0, validator.ConfigValidationExceptions.Count,
- "Root and child configs both with autoentities that resolved entities should pass validation.");
- }
-
///
/// Helper: verifies that the autoentity-discovered-zero warning was logged at least once,
/// optionally also checking that the formatted message contains a child config file name.
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 9b7d94914e..bdb5e2d390 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -6679,9 +6679,6 @@ public async Task TestValidate_MultiConfigRootResolvingZero_ProducesRootScopedEr
// Point the loader at the root config and load through the provider so the child is merged.
loader.UpdateConfigFilePath(rootConfigPath);
RuntimeConfigProvider provider = new(loader);
- Assert.IsTrue(provider.TryGetConfig(out RuntimeConfig mergedRoot) && mergedRoot is not null,
- "Root config with data-source-files should load and merge the child config.");
- Assert.AreEqual(1, mergedRoot.ChildConfigs.Count, "The child config should have been merged in.");
ILoggerFactory loggerFactory = new LoggerFactory();
RuntimeConfigValidator validator = new(
@@ -6719,25 +6716,13 @@ private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoa
// data-source-files through the real file system) can find and merge the child config.
IFileSystem fileSystem = new FileSystem();
- // Child: own MSSQL data source + autoentities matching a real table (dbo.books).
- RuntimeConfig childConfig = new(
- Schema: "child-schema",
- DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
- Entities: new(new Dictionary()),
- Autoentities: new(BuildAutoentityMap(definitionName: "child-filter", patternInclude: "dbo.books")),
- Runtime: new(
- Rest: new(),
- GraphQL: new(),
- Mcp: new(),
- Host: new(null, null, HostMode.Development)));
- File.WriteAllText("dab-child.json", childConfig.ToJson());
-
// Root: own MSSQL data source + autoentities (pattern controls whether it resolves) +
// data-source-files pointing at the child.
RuntimeConfig rootConfig = new(
Schema: "root-schema",
- DataSource: null,
+ DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
Entities: new(new Dictionary()),
+ Autoentities: new(BuildAutoentityMap(definitionName: "root-filter", patternInclude: "dbo.books", entiityNames: "root_{object}")),
Runtime: new(
Rest: new(),
GraphQL: new(),
@@ -6746,6 +6731,19 @@ private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoa
DataSourceFiles: new DataSourceFiles(new[] { "dab-child.json" }));
File.WriteAllText("dab-root.json", rootConfig.ToJson());
+ // Child: own MSSQL data source + autoentities matching a real table (dbo.books).
+ RuntimeConfig childConfig = new(
+ Schema: "child-schema",
+ DataSource: new(DatabaseType.MSSQL, connectionString, Options: null),
+ Entities: new(new Dictionary()),
+ Autoentities: new(BuildAutoentityMap(definitionName: "child-filter", patternInclude: "dbo.books", entiityNames: "child_{object}")),
+ Runtime: new(
+ Rest: new(),
+ GraphQL: new(),
+ Mcp: new(),
+ Host: new(null, null, HostMode.Development)));
+ File.WriteAllText("dab-child.json", childConfig.ToJson());
+
return ("dab-root.json", new FileSystemRuntimeConfigLoader(fileSystem), fileSystem);
}
@@ -6753,7 +6751,7 @@ private static (string RootConfigPath, FileSystemRuntimeConfigLoader ValidateLoa
/// Helper: builds an autoentity map containing a single definition whose include pattern
/// controls which tables it resolves against the target database.
///
- private static Dictionary BuildAutoentityMap(string definitionName, string patternInclude)
+ private static Dictionary BuildAutoentityMap(string definitionName, string patternInclude, string entiityNames)
{
EntityAction entityAction = new(EntityActionOperation.Read, null, null);
@@ -6761,7 +6759,7 @@ private static Dictionary BuildAutoentityMap(string definiti
Patterns: new AutoentityPatterns(
Include: new[] { patternInclude },
Exclude: Array.Empty(),
- Name: "{object}"),
+ Name: entiityNames),
Template: new AutoentityTemplate(
Rest: new(Enabled: true),
GraphQL: new(Enabled: true, Singular: string.Empty, Plural: string.Empty)),
From 73a2235826978db26134ba1a7ec9777cb57eab46 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Thu, 24 Sep 2026 17:23:06 -0700
Subject: [PATCH 10/11] Fix syntax
---
src/Service.Tests/Configuration/ConfigurationTests.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index bdb5e2d390..490ebe85c2 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -50,7 +50,6 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
From d32e4ed6e9ff645ecd3916a4fbe3ebb73f38a8d4 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Fri, 25 Sep 2026 12:00:41 -0700
Subject: [PATCH 11/11] Fix syntax errors
---
src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 490ebe85c2..0619622a9d 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -6685,7 +6685,7 @@ public async Task TestValidate_MultiConfigRootResolvingZero_ProducesRootScopedEr
fileSystem,
loggerFactory.CreateLogger(),
isValidateOnly: true);
-
+
// Runs metadata initialization (real autoentity resolution against MSSQL) + presence validation.
bool isValid = await validator.TryValidateConfig(rootConfigPath, loggerFactory);
Assert.IsTrue(isValid, "Validation should succeed");