From d3a8b108701cbc40462c3648ff90b6d0cb879bac Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:16:57 -0700 Subject: [PATCH 1/4] Update autoentities when hot-reload succeeds (#3297) ## Why make this change? - #3068 - #3304 Currently the inclusion of the autoentities feature causes hot-reload to fail when it should succeed. ## What is this change? Now the autoentities are updated when the hot-reload succeeds by ensuring that the generated entities from the autoentities property are removed if the initialization to connect to a database is done specifically for validation purposes. Since currently, hot-reload first validates before running the actual initialization, which would cause the autoentities to fail by trying to add the generated entities that already existed. - `Entity.cs` & `MsSqlMetadataProvider.cs`: Added new parameter that shows if an entity was generated through the autoentities property or just a regular entity. - `RuntimeConfigProvider.cs`, `RuntimeConfig.cs` & `SqlMetadataProvider.cs`: Added function that removes the generated autoentities from the RuntimeConfig object and also removes the relation between the autoentity with the data source. - `ConfigurationHotReloadTests.cs`: Added test to ensure autoentities works with hot-reload. - `dab.draft.schema.json`: Fixed small bug that caused validation to fail when using the `autoentities.mcp.dml-tools`. If the validation fails it also causes hot-reload to fail. ## How was this tested? - [ ] Integration Tests - [X] Unit Tests --------- Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com> --- schemas/dab.draft.schema.json | 22 ++++--- src/Cli.Tests/ModuleInitializer.cs | 2 + src/Config/ObjectModel/Entity.cs | 7 ++- src/Config/ObjectModel/RuntimeConfig.cs | 5 ++ .../Configurations/RuntimeConfigProvider.cs | 28 +++++++++ .../MsSqlMetadataProvider.cs | 3 +- .../MetadataProviders/SqlMetadataProvider.cs | 15 +++++ .../HotReload/ConfigurationHotReloadTests.cs | 59 +++++++++++++++++++ src/Service.Tests/ModuleInitializer.cs | 2 + 9 files changed, 134 insertions(+), 9 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 70d5c4be29..84f8e5cfbd 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -819,15 +819,23 @@ "additionalProperties": false, "properties": { "mcp": { - "type": "object", - "description": "MCP endpoint configuration", - "additionalProperties": false, - "properties": { - "dml-tools": { + "oneOf": [ + { "$ref": "#/$defs/boolean-or-string", - "description": "Enable/disable all DML tools with default settings." + "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality." + }, + { + "type": "object", + "description": "MCP endpoint configuration", + "additionalProperties": false, + "properties": { + "dml-tools": { + "$ref": "#/$defs/boolean-or-string", + "description": "Enable/disable all DML tools with default settings." + } + } } - } + ] }, "rest": { "type": "object", diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index 0bbb5b3eab..3a10eeffde 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -65,6 +65,8 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.EntityFirst); // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); + // Ignore the entity IsAutoentity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsAutoentity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index d67a1f9f28..9c9ba17f2c 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -45,6 +45,9 @@ public record Entity [JsonIgnore] public bool IsLinkingEntity { get; init; } + [JsonIgnore] + public bool IsAutoentity { get; init; } + [JsonConstructor] public Entity( EntitySource Source, @@ -58,7 +61,8 @@ public Entity( bool IsLinkingEntity = false, EntityHealthCheckConfig? Health = null, string? Description = null, - EntityMcpOptions? Mcp = null) + EntityMcpOptions? Mcp = null, + bool IsAutoentity = false) { this.Health = Health; this.Source = Source; @@ -72,6 +76,7 @@ public Entity( this.IsLinkingEntity = IsLinkingEntity; this.Description = Description; this.Mcp = Mcp; + this.IsAutoentity = IsAutoentity; } /// diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 89cc7413d1..659914a7ac 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -269,6 +269,11 @@ public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, str return false; } + public bool RemoveGeneratedAutoentityNameFromDataSourceName(string entityName) + { + return _entityNameToDataSourceName.Remove(entityName); + } + /// /// Constructor for runtimeConfig. /// To be used when setting up from cli json scenario. diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 644782e2cc..70327326ba 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -426,4 +426,32 @@ public void AddMergedEntitiesToConfig(Dictionary newEntities) }; _configLoader.EditRuntimeConfig(newRuntimeConfig); } + + public void RemoveGeneratedAutoentitiesFromConfig() + { + Dictionary entities = new(_configLoader.RuntimeConfig!.Entities); + List removingEntities = new(); + + // Add entities that will be removed to a list first to avoid modifying the collection while iterating over it. + foreach ((string name, Entity entity) in entities) + { + if (entity.IsAutoentity) + { + removingEntities.Add(name); + } + } + + // Remove all autoentities from the config. + foreach (string name in removingEntities) + { + entities.Remove(name); + _configLoader.RuntimeConfig!.RemoveGeneratedAutoentityNameFromDataSourceName(name); + } + + RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with + { + Entities = new(entities) + }; + _configLoader.EditRuntimeConfig(newRuntimeConfig); + } } diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 96fa47dcfd..297c8e2743 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -349,7 +349,8 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona Health: autoentity.Template.Health, Fields: null, Relationships: null, - Mappings: new()); + Mappings: new(), + IsAutoentity: true); // Add the generated entity to the linking entities dictionary. // This allows the entity to be processed later during metadata population. diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 6aa2712468..0ea9d25eb6 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -353,6 +353,12 @@ public async Task InitializeAsync() GenerateRestPathToEntityMap(); InitODataParser(); + + if (_isValidateOnly) + { + RemoveGeneratedAutoentities(); + } + timer.Stop(); _logger.LogTrace($"Done inferring Sql database schema in {timer.ElapsedMilliseconds}ms."); } @@ -714,6 +720,15 @@ protected virtual Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary + /// Removes the entities that were generated from the autoentities property. + /// This should only be done when we only want to validate the entities. + /// + private void RemoveGeneratedAutoentities() + { + _runtimeConfigProvider.RemoveGeneratedAutoentitiesFromConfig(); + } + protected void PopulateDatabaseObjectForEntity( Entity entity, string entityName, diff --git a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs index 0f9ff6c1b8..b1770f504d 100644 --- a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs +++ b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs @@ -64,6 +64,7 @@ private static void GenerateConfigFile( string entityBackingColumn = "title", string entityExposedName = "title", string mcpEnabled = "true", + string autoentityName = "autoentity_{object}", string configFileName = CONFIG_FILE_NAME) { File.WriteAllText(configFileName, @" @@ -180,6 +181,29 @@ private static void GenerateConfigFile( } ] } + }, + ""autoentities"": { + ""BooksAutoentities"": { + ""patterns"": { + ""include"": [ ""%book%"" ], + ""name"": """ + autoentityName + @""" + }, + ""template"": { + ""rest"": { + ""enabled"": true + } + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [ + { + ""action"": ""*"" + } + ] + } + ] + } } }"); } @@ -768,6 +792,41 @@ await WaitForConditionAsync( Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode); } + /// + /// Hot reload the configuration file so that it changes the name of the autoentity properties. + /// Then we assert that the hot reload is successful by sending a request to the newly created autoentity. + /// + [TestCategory(MSSQL_ENVIRONMENT)] + [TestMethod] + public async Task HotReloadAutoentities() + { + // Arrange + _writer = new StringWriter(); + Console.SetOut(_writer); + + // Act + HttpResponseMessage restResult = await _testClient.GetAsync($"rest/autoentity_books"); + + GenerateConfigFile( + connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}", + autoentityName: "HotReload_{object}"); + await WaitForConditionAsync( + () => WriterContains(HOT_RELOAD_SUCCESS_MESSAGE), + TimeSpan.FromSeconds(HOT_RELOAD_TIMEOUT_SECONDS), + TimeSpan.FromMilliseconds(500)); + + HttpResponseMessage failRestResult = await _testClient.GetAsync($"rest/autoentity_books"); + HttpResponseMessage hotReloadRestResult = await _testClient.GetAsync($"rest/HotReload_books"); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, restResult.StatusCode, + $"REST request before hot-reload failed when it was expected to succeed. Response: {await restResult.Content.ReadAsStringAsync()}"); + Assert.AreEqual(HttpStatusCode.NotFound, failRestResult.StatusCode, + $"REST request after hot-reload succeeded when it was expected to fail. Response: {await failRestResult.Content.ReadAsStringAsync()}"); + Assert.AreEqual(HttpStatusCode.OK, hotReloadRestResult.StatusCode, + $"REST request after hot-reload failed when it was expected to succeed. Response: {await hotReloadRestResult.Content.ReadAsStringAsync()}"); + } + /// /// /// (Warning: This test only currently works in the pipeline due to constrains of not /// being able to change from one database type to another, under normal circumstances diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index 637280b45d..9d8c213b7c 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -69,6 +69,8 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.EntityFirst); // Ignore the entity IsLinkingEntity as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); + // Ignore the entity IsAutoentity as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(entity => entity.IsAutoentity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); // Ignore the UserProvidedEnabledOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. From 3208f621b671e585830a41e00a1af43ca7b80a79 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:10:43 -0700 Subject: [PATCH 2/4] Fix logs for autoentities (#3299) ## Why make this change? - Closes #3263 - Closes #3264 - Closes #3265 Currently some of the logs for autoentities are not completely clear or they are too noisy. Which is why the messages need to be changed. ## What is this change? This changes the log messages so they are more clear by adding `' '` to the definition names from the autoentities, and stating that they are definition names beforehand. We also change the name from `Autoentities` to `autoentities` in the logs so that they are all consistent. Lastly we also change some of the logs from `Information` to `Debug`. ## How was this tested? Tested locally since all the changes are related to the messages written in the logs. --- src/Cli/ConfigGenerator.cs | 4 ++-- src/Config/ObjectModel/RuntimeConfig.cs | 2 +- .../MsSqlMetadataProvider.cs | 20 +++++++++---------- .../MetadataProviders/SqlMetadataProvider.cs | 2 +- .../Configuration/ConfigurationTests.cs | 2 +- .../UnitTests/SqlMetadataProviderUnitTests.cs | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 8cec9dd239..f9fb54f06d 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -3268,7 +3268,7 @@ public static bool TrySimulateAutoentities(AutoConfigSimulateOptions options, Fi if (runtimeConfig.DataSource.DatabaseType != DatabaseType.MSSQL) { - _logger.LogError("Autoentities simulation is only supported for MSSQL databases. Current database type: {DatabaseType}.", runtimeConfig.DataSource.DatabaseType); + _logger.LogError("The autoentities simulation is only supported for MSSQL databases. Current database type: {DatabaseType}.", runtimeConfig.DataSource.DatabaseType); return false; } @@ -3360,7 +3360,7 @@ public static bool TrySimulateAutoentities(AutoConfigSimulateOptions options, Fi /// The simulation results keyed by filter (definition) name. private static void WriteSimulationResultsToConsole(Dictionary> results) { - Console.WriteLine("AutoEntities Simulation Results"); + Console.WriteLine("Autoentities Simulation Results"); Console.WriteLine(); foreach ((string filterName, List<(string EntityName, string SchemaName, string ObjectName)> matches) in results) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 659914a7ac..54c3e77556 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -507,7 +507,7 @@ public string GetDataSourceNameFromAutoentityName(string autoentityName) if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource)) { throw new DataApiBuilderException( - message: $"{autoentityName} is not a valid autoentity.", + message: $"'{autoentityName}' is not a valid autoentities definition.", statusCode: HttpStatusCode.NotFound, subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); } diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 297c8e2743..f85249b69e 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -305,7 +305,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona foreach ((string autoentityName, Autoentity autoentity) in autoentities) { int addedEntities = 0; - JsonArray? resultArray = await QueryAutoentitiesAsync(autoentity); + JsonArray? resultArray = await QueryAutoentitiesAsync(autoentityName, autoentity); if (resultArray is null) { continue; @@ -316,7 +316,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (resultObject is null) { throw new DataApiBuilderException( - message: $"Cannot create new entity from autoentity pattern due to an internal error.", + message: $"Cannot create new entity from autoentities definition '{autoentityName}' due to an internal error.", statusCode: HttpStatusCode.InternalServerError, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -329,7 +329,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (string.IsNullOrWhiteSpace(entityName) || string.IsNullOrWhiteSpace(objectName) || string.IsNullOrWhiteSpace(schemaName)) { - _logger.LogError("Skipping autoentity generation: entity_name or object is null or empty for autoentity pattern '{AutoentityName}'.", autoentityName); + _logger.LogError("Skipping autoentity generation: 'entity_name', 'object', or 'schema' is null or empty for autoentities definition '{autoentityName}'.", autoentityName); continue; } @@ -357,7 +357,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddGeneratedAutoentityNameToDataSourceName(entityName, autoentityName)) { throw new DataApiBuilderException( - message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentity pattern with definition-name '{autoentityName}'.", + message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentities definition '{autoentityName}'.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -376,14 +376,14 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (addedEntities == 0) { - _logger.LogWarning("No new entities were generated from the autoentity {autoentityName} defined in the configuration.", autoentityName); + _logger.LogWarning("No new entities were generated from the autoentities definition '{autoentityName}'.", autoentityName); } } _runtimeConfigProvider.AddMergedEntitiesToConfig(entities); } - public async Task QueryAutoentitiesAsync(Autoentity autoentity) + public async Task QueryAutoentitiesAsync(string autoentityName, Autoentity autoentity) { string include = string.Join(",", autoentity.Patterns.Include); string exclude = string.Join(",", autoentity.Patterns.Exclude); @@ -396,10 +396,10 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona { $"{BaseQueryStructure.PARAM_NAME_PREFIX}name_pattern", new(namePattern, null, SqlDbType.NVarChar) } }; - _logger.LogInformation("Query for Autoentities is being executed with the following parameters."); - _logger.LogInformation($"Autoentities include pattern: {include}"); - _logger.LogInformation($"Autoentities exclude pattern: {exclude}"); - _logger.LogInformation($"Autoentities name pattern: {namePattern}"); + _logger.LogDebug("Query for autoentities is being executed with the following parameters."); + _logger.LogDebug("The autoentities definition '{autoentityName}' include pattern: {include}", autoentityName, include); + _logger.LogDebug("The autoentities definition '{autoentityName}' exclude pattern: {exclude}", autoentityName, exclude); + _logger.LogDebug("The autoentities definition '{autoentityName}' name pattern: {namePattern}", autoentityName, namePattern); JsonArray? resultArray = await QueryExecutor.ExecuteQueryAsync( sqltext: getAutoentitiesQuery, diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 0ea9d25eb6..951b5984e4 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -717,7 +717,7 @@ private void GenerateDatabaseObjectForEntities() /// protected virtual Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary? autoentities) { - throw new NotSupportedException($"{GetType().Name} does not support Autoentities yet."); + throw new NotSupportedException($"{GetType().Name} does not support autoentities yet."); } /// diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 53a97ae722..cc03c94625 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -5590,7 +5590,7 @@ public async Task TestAutoentitiesAreGeneratedIntoEntities(bool useEntities, int /// [TestCategory(TestCategory.MSSQL)] [DataTestMethod] - [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity with name 'publishers' already exists. Cannot create new entity from autoentity pattern with definition-name 'PublisherAutoEntity'.", DisplayName = "Autoentities fail due to entity name")] + [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity with name 'publishers' already exists. Cannot create new entity from autoentities definition 'PublisherAutoEntity'.", DisplayName = "Autoentities fail due to entity name")] [DataRow("UniquePublisher", "publishers", "uniquePluralPublishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql singular type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "publishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql plural type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "uniquePluralPublishers", "/publishers", "The rest path: publishers specified for entity: publishers is already used by another entity.", DisplayName = "Autoentities fail due to rest path")] diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index 4c5782b4ca..7242716847 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -634,7 +634,7 @@ public async Task CheckAutoentitiesQuery(string[] include, string[] exclude, str // Act MsSqlMetadataProvider metadataProvider = (MsSqlMetadataProvider)_sqlMetadataProvider; - JsonArray resultArray = await metadataProvider.QueryAutoentitiesAsync(autoentity); + JsonArray resultArray = await metadataProvider.QueryAutoentitiesAsync("autoentity", autoentity); // Assert Assert.IsNotNull(resultArray); From a93cc493616431688b9d8543b3732d957221875c Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:24:53 -0700 Subject: [PATCH 3/4] Fix autoentity validation & CLI/Schema mismatch bugs (#3416) ## Why make this change? - #3375 - The CLI and the schema have a mismatch in the `autoentities..template.mcp.dml-tools`, need to ensure that they are the same. - #3335 - Using `dab validate` produces the wrong output message. ## What is this change? For issue #3375: - We changed the `AutoConfigOption.cs` file so that it uses the proper name and changed the name of the variable to also match the schema in the `ConfigGenerator.cs`. For issue #3335: - We changed the log message in `MsSqlMetadataProvider.cs` so that it is easier for the user to understand the error. ## How was this tested? - [ ] Integration Tests - [ ] Unit Tests - [x] Local Testing The issues were related to mismatches or to the output of log messages that can only be tested locally. ## Sample Request(s) dab auto-config --template.mcp.dml-tools true/false dab validate --config test.json --------- Co-authored-by: Souvik Ghosh Co-authored-by: Aniruddh Munde --- src/Cli.Tests/AutoConfigTests.cs | 4 ++-- src/Cli/Commands/AutoConfigOptions.cs | 8 ++++---- src/Cli/ConfigGenerator.cs | 8 ++++---- .../Services/MetadataProviders/MsSqlMetadataProvider.cs | 2 +- src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Cli.Tests/AutoConfigTests.cs b/src/Cli.Tests/AutoConfigTests.cs index 40e3a461f7..7bd5487305 100644 --- a/src/Cli.Tests/AutoConfigTests.cs +++ b/src/Cli.Tests/AutoConfigTests.cs @@ -80,7 +80,7 @@ public void TestConfigureAutoentitiesDefinition_WithTemplateOptions() definitionName: "test-def", templateRestEnabled: true, templateGraphqlEnabled: false, - templateMcpDmlTool: "true", + templateMcpDmlTools: "true", templateCacheEnabled: true, templateCacheTtlSeconds: 30, templateCacheLevel: "L1", @@ -196,7 +196,7 @@ public void TestConfigureAutoentitiesDefinition_InvalidMcpDmlTool() AutoConfigOptions options = new( definitionName: "test-def", - templateMcpDmlTool: "invalid-value", + templateMcpDmlTools: "invalid-value", permissions: new[] { "anonymous", "read" }, config: TEST_RUNTIME_CONFIG_FILE ); diff --git a/src/Cli/Commands/AutoConfigOptions.cs b/src/Cli/Commands/AutoConfigOptions.cs index 41227cd03a..41be943303 100644 --- a/src/Cli/Commands/AutoConfigOptions.cs +++ b/src/Cli/Commands/AutoConfigOptions.cs @@ -24,7 +24,7 @@ public AutoConfigOptions( IEnumerable? patternsInclude = null, IEnumerable? patternsExclude = null, string? patternsName = null, - string? templateMcpDmlTool = null, + string? templateMcpDmlTools = null, bool? templateRestEnabled = null, bool? templateGraphqlEnabled = null, bool? templateCacheEnabled = null, @@ -39,7 +39,7 @@ public AutoConfigOptions( PatternsInclude = patternsInclude; PatternsExclude = patternsExclude; PatternsName = patternsName; - TemplateMcpDmlTool = templateMcpDmlTool; + TemplateMcpDmlTools = templateMcpDmlTools; TemplateRestEnabled = templateRestEnabled; TemplateGraphqlEnabled = templateGraphqlEnabled; TemplateCacheEnabled = templateCacheEnabled; @@ -61,8 +61,8 @@ public AutoConfigOptions( [Option("patterns.name", Required = false, HelpText = "Interpolation syntax for entity naming (must be unique for each generated entity). Default: '{object}'")] public string? PatternsName { get; } - [Option("template.mcp.dml-tool", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false. Default: true")] - public string? TemplateMcpDmlTool { get; } + [Option("template.mcp.dml-tools", Required = false, HelpText = "Enable/disable DML tools for generated entities. Allowed values: true, false. Default: true")] + public string? TemplateMcpDmlTools { get; } [Option("template.rest.enabled", Required = false, HelpText = "Enable/disable REST endpoint for generated entities. Allowed values: true, false. Default: true")] public bool? TemplateRestEnabled { get; } diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index f9fb54f06d..47b0454da4 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -3109,11 +3109,11 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoConfigOptions opti bool userProvidedCache = existingAutoentity?.Template.UserProvidedCacheOptions ?? false; // Update MCP options - if (!string.IsNullOrWhiteSpace(options.TemplateMcpDmlTool)) + if (!string.IsNullOrWhiteSpace(options.TemplateMcpDmlTools)) { - if (!bool.TryParse(options.TemplateMcpDmlTool, out bool mcpDmlToolValue)) + if (!bool.TryParse(options.TemplateMcpDmlTools, out bool mcpDmlToolValue)) { - _logger.LogError("Invalid value for template.mcp.dml-tool: {value}. Valid values are: true, false", options.TemplateMcpDmlTool); + _logger.LogError("Invalid value for template.mcp.dml-tools: {value}. Valid values are: true, false", options.TemplateMcpDmlTools); return null; } @@ -3122,7 +3122,7 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoConfigOptions opti bool? dmlToolValue = mcpDmlToolValue; mcp = new EntityMcpOptions(customToolEnabled: customToolEnabled, dmlToolsEnabled: dmlToolValue); userProvidedMcp = true; - _logger.LogInformation("Updated template.mcp.dml-tool for definition '{DefinitionName}'", options.DefinitionName); + _logger.LogInformation("Updated template.mcp.dml-tools for definition '{DefinitionName}'", options.DefinitionName); } // Update REST options diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index f85249b69e..23d12ec31f 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -357,7 +357,7 @@ protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictiona if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddGeneratedAutoentityNameToDataSourceName(entityName, autoentityName)) { throw new DataApiBuilderException( - message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentities definition '{autoentityName}'.", + message: $"Entity '{entityName}' conflicts with autoentity pattern '{autoentityName}'. Use --patterns.exclude to skip it.", statusCode: HttpStatusCode.BadRequest, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index cc03c94625..e37b0920e2 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -5590,7 +5590,7 @@ public async Task TestAutoentitiesAreGeneratedIntoEntities(bool useEntities, int /// [TestCategory(TestCategory.MSSQL)] [DataTestMethod] - [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity with name 'publishers' already exists. Cannot create new entity from autoentities definition 'PublisherAutoEntity'.", DisplayName = "Autoentities fail due to entity name")] + [DataRow("publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity 'publishers' conflicts with autoentity pattern 'PublisherAutoEntity'. Use --patterns.exclude to skip it.", DisplayName = "Autoentities fail due to entity name")] [DataRow("UniquePublisher", "publishers", "uniquePluralPublishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql singular type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "publishers", "/unique/publisher", "Entity publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql plural type")] [DataRow("UniquePublisher", "uniqueSingularPublisher", "uniquePluralPublishers", "/publishers", "The rest path: publishers specified for entity: publishers is already used by another entity.", DisplayName = "Autoentities fail due to rest path")] From 4aebcde7f7c9315d4be398e922258f7733b17fff Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:42:12 -0700 Subject: [PATCH 4/4] Fix CLI overrides LogLevel value from config file (#3426) ## Why make this change? - Closes issue #3258 Whenever we use the `dab start` command the CLI overrides the minimum LogLevel from the configuration file. This is acceptable only if we use the `--LogLevel` flag. If we do not use that flag, the LogLevel for each namespace should be decided by the configuration file. ## What is this change? This change moves the `args.Add` methods inside the `ConfigGenerator.cs` so that they are only applied when we use the `--LogLevel` flag. Having these arguments causes DAB to not allow any changes to the LogLevel in the loggers, since those arguments were always being added, DAB always assumed the CLI was overriding the LogLevel when it was not expected as it would use the `--LogLevel` flag to determine if it was an `IsLogLevelOverridenByCli` scenario. ## How was this tested? - [ ] Integration Tests - [x] Unit Tests - [x] Manual Tests Tested with different configurations of the namespaces in the `log-level` property inside the config file. Note: ALL these manual tests were tested by running the CLI as well as running DAB through Visual Studio. Config file includes: ``` "log-level": { "Azure.DataApiBuilder.Core.Services.ISqlMetadataProvider": "Information", "Azure.DataApiBuilder.Core": "Debug", "Azure.DataApiBuilder.Config.FileSystemRuntimeConfigLoader": "Error", "default": "Warning" } ``` Expected results: - All the logs from `Azure.DataApiBuilder.Core.Services.ISqlMetadataProvider` will provide logs from log level information and above. - All logs from `Azure.DataApiBuilder.Core` and its sub-namespaces that are not from `ISqlMetadataProvider` will provide logs from log level debug and above. - All logs from `Azure.DataApiBuilder.Config.FileSystemRuntimeConfigLoader` will provide logs from log level error and above. - Everything else will provide logs from log level warning and above, including CLI. Config file includes: ``` "log-level": { "Azure.DataApiBuilder.Core": "Information", "Azure.DataApiBuilder.Config": "Debug", } ``` Expected results: - All logs from `Azure.DataApiBuilder.Core` and its sub-namespaces will provide logs from log level information and above. - All logs from `Azure.DataApiBuilder.Config` and its sub-namespaces will provide logs from log level debug and above. - Everything else will provide logs from log level debug and above with `host.mode = development`, including CLI. Config file includes: ``` "log-level": { "Default: none } ``` Expected results: - No logs will be printed, including CLI. ## Sample Request(s) dab start --LogLevel information dab start (With config file default value debug) --- src/Cli.Tests/EndToEndTests.cs | 78 ++++++++++++++++++++++++++++++++++ src/Cli/ConfigGenerator.cs | 12 +----- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 4ae27f790f..d5bd937fe9 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -3,6 +3,7 @@ using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Product; +using Azure.DataApiBuilder.Service; using Cli.Constants; using Microsoft.Data.SqlClient; @@ -857,6 +858,83 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption StringAssert.Contains(output, $"User provided config file: {TEST_RUNTIME_CONFIG_FILE}", StringComparison.Ordinal); } + /// + /// Validates that `dab start` correctly sets + /// based on whether the --LogLevel CLI flag is provided. + /// + /// When the --LogLevel flag is provided, IsLogLevelOverriddenByCli should be true. + /// When the --LogLevel flag is omitted (log level comes from the config file), IsLogLevelOverriddenByCli should be false. + /// + /// The --LogLevel CLI flag value, or null to omit the flag. + /// Expected value of Startup.IsLogLevelOverriddenByCli. + [DataTestMethod] + [DataRow(null, false, DisplayName = "IsLogLevelOverriddenByCli is false")] + [DataRow(LogLevel.Error, true, DisplayName = "IsLogLevelOverriddenByCli is true")] + public async Task TestStartCommandResolvesLogLevelFromConfigOrFlag( + LogLevel? cliLogLevel, + bool expectedIsOverridden) + { + string baseConfig = @" + { + ""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @""", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": """ + SAMPLE_TEST_CONN_STRING + @""" + }, + ""runtime"": { + ""rest"": { + ""path"": ""/api"", + ""enabled"": true + }, + ""graphql"": { + ""path"": ""/graphql"", + ""enabled"": true, + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""Unauthenticated"" + } + }, + ""telemetry"": { + ""log-level"": { + ""Azure.DataApiBuilder.Core.Services.ISqlMetadataProvider"": ""Information"", + ""Azure.DataApiBuilder.Core"": ""Debug"", + ""Azure.DataApiBuilder.Service.Controllers.RestController"": ""Error"", + ""default"": ""Warning"" + } + } + }, + ""entities"": {} + }"; + + // Merge in an entity so the config is not rejected for having an empty entities section. + string configWithLogLevel = AddPropertiesToJson(baseConfig, BASIC_ENTITY_WITH_ANONYMOUS_ROLE); + _fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, configWithLogLevel); + + StartOptions options = new( + verbose: false, + logLevel: cliLogLevel, + isHttpsRedirectionDisabled: false, + mcpStdio: false, + mcpRole: null, + config: TEST_RUNTIME_CONFIG_FILE); + + // Run TryStartEngineWithOptions on a background task because StartEngine blocks until the host shuts down. + Task engineTask = Task.Run(() => + TryStartEngineWithOptions(options, _runtimeConfigLoader!, _fileSystem!)); + + // Wait for the engine to finish loading the config. + await Task.Delay(TimeSpan.FromSeconds(5)); + + Assert.AreEqual(expectedIsOverridden, Startup.IsLogLevelOverriddenByCli); + } + /// /// Validates that valid usage of verbs and associated options produce exit code 0 (CliReturnCode.SUCCESS). /// Verifies that explicitly implemented verbs (add, update, init, start) and appropriately diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 47b0454da4..af98be05ff 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2596,18 +2596,10 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun } minimumLogLevel = (LogLevel)options.LogLevel; + args.Add("--LogLevel"); + args.Add(minimumLogLevel.ToString()); _logger.LogInformation("Setting minimum LogLevel: {minimumLogLevel}.", minimumLogLevel); } - else - { - minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel(); - HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production; - - _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType); - } - - args.Add("--LogLevel"); - args.Add(minimumLogLevel.ToString()); // This will add args to disable automatic redirects to https if specified by user if (options.IsHttpsRedirectionDisabled)