From 2f4430832b09d16542fe962c11330b646fa9793d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 18:29:30 +0000
Subject: [PATCH 1/2] Initial plan
From 95bcddf498191643ac1ce811f630d67079ba0600 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 19:15:29 +0000
Subject: [PATCH 2/2] Enforce runtime config minimum property requirements
Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com>
---
schemas/dab.draft.schema.json | 32 +++++++++----
src/Cli.Tests/TestHelper.cs | 16 +++++++
src/Cli.Tests/ValidateConfigTests.cs | 47 ++++++++++++++++++
src/Config/ObjectModel/RuntimeConfig.cs | 35 +++++++++-----
src/Config/RuntimeConfigLoader.cs | 63 +++++++++++++------------
5 files changed, 143 insertions(+), 50 deletions(-)
diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index fa5208af66..0173949786 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -1433,17 +1433,31 @@
}
}
},
- "if": {
- "required": ["azure-key-vault"]
- },
- "then": {
- "properties": {
- "azure-key-vault": {
- "required": ["endpoint"]
+ "allOf": [
+ {
+ "if": { "required": ["azure-key-vault"] },
+ "then": {
+ "properties": {
+ "azure-key-vault": {
+ "required": ["endpoint"]
+ }
+ }
}
+ },
+ {
+ "if": { "not": { "required": ["data-source-files"] } },
+ "then": { "required": ["data-source"] }
+ },
+ {
+ "if": {
+ "allOf": [
+ { "not": { "required": ["autoentities"] } },
+ { "not": { "required": ["data-source-files"] } }
+ ]
+ },
+ "then": { "required": ["entities"] }
}
- },
- "required": ["data-source", "entities"],
+ ],
"$defs": {
"singular-plural": {
"oneOf": [
diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs
index a75e359ee4..e2eb1fd544 100644
--- a/src/Cli.Tests/TestHelper.cs
+++ b/src/Cli.Tests/TestHelper.cs
@@ -279,6 +279,22 @@ public static Process ExecuteDabCommand(string command, string flags)
public const string CONFIG_WITH_DISABLED_GLOBAL_REST_GRAPHQL = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION_WITH_DISABLED_REST_GRAPHQL}}}";
+ ///
+ /// A valid config json using autoentities instead of entities. data-source is still required.
+ ///
+ public const string CONFIG_WITH_AUTOENTITIES_ONLY = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION}," + @"""autoentities"": { ""myAutoentity"": { ""patterns"": { ""include"": [""%.%""] } } } }";
+
+ ///
+ /// A valid multi-config (data-source-files) config without data-source or entities at the top level.
+ /// Only runtime is required in this scenario.
+ ///
+ public const string CONFIG_WITH_DATA_SOURCE_FILES_ONLY = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""data-source-files"": [""child-config.json""] }";
+
+ ///
+ /// A config without data-source and without data-source-files. This is invalid.
+ ///
+ public const string CONFIG_WITHOUT_DATA_SOURCE_OR_DATA_SOURCE_FILES = $"{{{SCHEMA_PROPERTY},{RUNTIME_SECTION}," + @"""entities"": {} }";
+
///
/// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing
/// user-delegated-auth configuration.
diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs
index e40a32e291..b14f5bba20 100644
--- a/src/Cli.Tests/ValidateConfigTests.cs
+++ b/src/Cli.Tests/ValidateConfigTests.cs
@@ -199,6 +199,53 @@ public void TestValidateConfigFailsWithNoEntities()
}
}
+ ///
+ /// Verifies that a config using autoentities (without an entities section) passes validation.
+ /// When autoentities is present, data-source is required but entities is not.
+ ///
+ [TestMethod]
+ public void TestValidateConfigPassesWithAutoentitiesAndNoEntities()
+ {
+ ((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, CONFIG_WITH_AUTOENTITIES_ONLY);
+
+ try
+ {
+ // Config with autoentities and data-source but no entities should parse successfully.
+ // Full validation may fail due to no live database, but config parsing should succeed.
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(
+ _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE), out RuntimeConfig? config));
+ Assert.IsNotNull(config);
+ Assert.IsTrue(config.Autoentities.Autoentities.Count > 0);
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unexpected Exception thrown: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Verifies that a multi-config (data-source-files) config without data-source or entities passes parsing.
+ /// When data-source-files is present, only runtime is required.
+ ///
+ [TestMethod]
+ public void TestValidateConfigPassesWithDataSourceFilesOnly()
+ {
+ ((MockFileSystem)_fileSystem!).AddFile(TEST_RUNTIME_CONFIG_FILE, CONFIG_WITH_DATA_SOURCE_FILES_ONLY);
+
+ try
+ {
+ // Config with data-source-files but no data-source or entities should parse successfully.
+ Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(
+ _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE), out RuntimeConfig? config));
+ Assert.IsNotNull(config);
+ Assert.IsNotNull(config.DataSourceFiles);
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unexpected Exception thrown: {ex.Message}");
+ }
+ }
+
///
/// This Test is used to verify that the validate command is able to catch when data source field is missing.
///
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index 46ff5a8dda..450113818e 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -267,29 +267,32 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str
/// To be used when setting up from cli json scenario.
///
/// schema for config.
- /// Default datasource.
+ /// Default datasource. May be null when data-source-files is specified (multi-config scenario).
/// Entities
/// Runtime settings.
/// List of datasource files for multiple db scenario. Null for single db scenario.
+#pragma warning disable CS8618 // DataSource may be null for multi-config (data-source-files) scenarios; callers should use ListAllDataSources() in that case.
[JsonConstructor]
public RuntimeConfig(
string? Schema,
- DataSource DataSource,
- RuntimeEntities Entities,
+ DataSource? DataSource,
+ RuntimeEntities? Entities,
RuntimeAutoentities? Autoentities = null,
RuntimeOptions? Runtime = null,
DataSourceFiles? DataSourceFiles = null,
AzureKeyVaultOptions? AzureKeyVault = null)
+#pragma warning restore CS8618
{
this.Schema = Schema ?? DEFAULT_CONFIG_SCHEMA_LINK;
- this.DataSource = DataSource;
+ this.DataSource = DataSource!; // May be null for multi-config (data-source-files) scenarios.
this.Runtime = Runtime;
this.AzureKeyVault = AzureKeyVault;
this.Entities = Entities ?? new RuntimeEntities(new Dictionary());
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary());
this.DefaultDataSourceName = Guid.NewGuid().ToString();
- if (this.DataSource is null)
+ // data-source is required unless data-source-files is specified (multi-config scenario).
+ if (this.DataSource is null && DataSourceFiles is null)
{
throw new DataApiBuilderException(
message: "data-source is a mandatory property in DAB Config",
@@ -297,14 +300,18 @@ public RuntimeConfig(
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}
- // we will set them up with default values
- _dataSourceNameToDataSource = new Dictionary
+ // Initialize data source dictionary; only add the default data source when it is present.
+ _dataSourceNameToDataSource = new Dictionary();
+ if (this.DataSource is not null)
{
- { this.DefaultDataSourceName, this.DataSource }
- };
+ _dataSourceNameToDataSource.Add(this.DefaultDataSourceName, this.DataSource);
+ }
_entityNameToDataSourceName = new Dictionary();
- if (Entities is null && this.Entities.Entities.Count == 0 &&
+ // entities/autoentities are required unless data-source-files is present (sub-configs supply them)
+ // or autoentities is present (entities are not required alongside autoentities).
+ if (DataSourceFiles is null &&
+ Entities is null && this.Entities.Entities.Count == 0 &&
Autoentities is null && this.Autoentities.Autoentities.Count == 0)
{
throw new DataApiBuilderException(
@@ -353,8 +360,12 @@ 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 == null
+ ? config.Entities.AsEnumerable()
+ : allEntities.Concat(config.Entities.AsEnumerable());
+ allAutoentities = allAutoentities == null
+ ? config.Autoentities.AsEnumerable()
+ : allAutoentities.Concat(config.Autoentities.AsEnumerable());
}
catch (Exception e)
{
diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs
index ae5c2dde95..6cadbdc924 100644
--- a/src/Config/RuntimeConfigLoader.cs
+++ b/src/Config/RuntimeConfigLoader.cs
@@ -219,43 +219,48 @@ public static bool TryParseConfig(string json,
return false;
}
- // retreive current connection string from config
- string updatedConnectionString = config.DataSource.ConnectionString;
-
- if (!string.IsNullOrEmpty(connectionString))
+ // For single-datasource configs, update the default data source's connection string.
+ // Multi-config (data-source-files) scenarios may have no default DataSource.
+ if (config.DataSource is not null)
{
- // update connection string if provided.
- updatedConnectionString = connectionString;
- }
+ // retreive current connection string from config
+ string updatedConnectionString = config.DataSource.ConnectionString;
- Dictionary datasourceNameToConnectionString = new();
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ // update connection string if provided.
+ updatedConnectionString = connectionString;
+ }
- // add to dictionary if datasourceName is present
- datasourceNameToConnectionString.TryAdd(config.DefaultDataSourceName, updatedConnectionString);
+ Dictionary datasourceNameToConnectionString = new();
- // iterate over dictionary and update runtime config with connection strings.
- foreach ((string dataSourceKey, string connectionValue) in datasourceNameToConnectionString)
- {
- string updatedConnection = connectionValue;
+ // add to dictionary if datasourceName is present
+ datasourceNameToConnectionString.TryAdd(config.DefaultDataSourceName, updatedConnectionString);
- DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey);
-
- // Add Application Name for telemetry for MsSQL or PgSql
- if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true)
- {
- updatedConnection = GetConnectionStringWithApplicationName(connectionValue);
- }
- else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true)
+ // iterate over dictionary and update runtime config with connection strings.
+ foreach ((string dataSourceKey, string connectionValue) in datasourceNameToConnectionString)
{
- updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue);
- }
+ string updatedConnection = connectionValue;
- ds = ds with { ConnectionString = updatedConnection };
- config.UpdateDataSourceNameToDataSource(config.DefaultDataSourceName, ds);
+ DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey);
- if (string.Equals(dataSourceKey, config.DefaultDataSourceName, StringComparison.OrdinalIgnoreCase))
- {
- config = config with { DataSource = ds };
+ // Add Application Name for telemetry for MsSQL or PgSql
+ if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true)
+ {
+ updatedConnection = GetConnectionStringWithApplicationName(connectionValue);
+ }
+ else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true)
+ {
+ updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue);
+ }
+
+ ds = ds with { ConnectionString = updatedConnection };
+ config.UpdateDataSourceNameToDataSource(config.DefaultDataSourceName, ds);
+
+ if (string.Equals(dataSourceKey, config.DefaultDataSourceName, StringComparison.OrdinalIgnoreCase))
+ {
+ config = config with { DataSource = ds };
+ }
}
}
}