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");