Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a9a4ea0
Changes for autoentities
RubenCerna2079 Jan 22, 2026
4acee5c
Add query and its execution
RubenCerna2079 Jan 26, 2026
67e10d7
Add testing
RubenCerna2079 Jan 27, 2026
5453cbc
Changes based on comments
RubenCerna2079 Jan 29, 2026
8fbea75
Changes based on comments
RubenCerna2079 Jan 29, 2026
cf4debf
Added changes based on comments
RubenCerna2079 Jan 30, 2026
88b8c66
Comment out failing section
RubenCerna2079 Jan 30, 2026
612d4bd
Fix test failures
RubenCerna2079 Jan 30, 2026
a396fab
Fix formatting issues
RubenCerna2079 Jan 30, 2026
c8ed0d5
Generate in-memory entities
RubenCerna2079 Feb 5, 2026
947fdec
Changes to generate autoentities as entities
RubenCerna2079 Feb 11, 2026
e5ae16a
Add new testing
RubenCerna2079 Feb 12, 2026
2f256e4
Add check so that either entities or autoentities is required
RubenCerna2079 Feb 12, 2026
6fca50d
Fix grammar errors
RubenCerna2079 Feb 13, 2026
8b88815
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 13, 2026
ba070db
Changes to fix tests
RubenCerna2079 Feb 13, 2026
5bd10a9
Changes based on comments
RubenCerna2079 Feb 17, 2026
33f33c3
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 17, 2026
3e00f77
Changes for autoentities
RubenCerna2079 Jan 22, 2026
84814fb
Add query and its execution
RubenCerna2079 Jan 26, 2026
5306d94
Add testing
RubenCerna2079 Jan 27, 2026
0cdf4c8
Changes based on comments
RubenCerna2079 Jan 29, 2026
1f8d4f5
Changes based on comments
RubenCerna2079 Jan 29, 2026
09f72ac
Added changes based on comments
RubenCerna2079 Jan 30, 2026
e470a31
Fix formatting issues
RubenCerna2079 Jan 30, 2026
83d706f
Fix bug
RubenCerna2079 Feb 18, 2026
9ab35a6
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 18, 2026
73f9633
Merge remote-tracking branch 'refs/remotes/origin/dev/rubencerna/crea…
RubenCerna2079 Feb 18, 2026
4a743c7
Changes to ensure autoentities work with multiple data sources
RubenCerna2079 Feb 20, 2026
7570931
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 20, 2026
28614aa
Changes based on comments
RubenCerna2079 Feb 23, 2026
b6a0bc2
Fix Tests
RubenCerna2079 Feb 25, 2026
97577de
Merge remote-tracking branch 'refs/remotes/origin/dev/rubencerna/crea…
RubenCerna2079 Feb 25, 2026
1c568a5
Fix tests
RubenCerna2079 Feb 25, 2026
7b499a7
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 25, 2026
56c744a
Fix tests
RubenCerna2079 Feb 26, 2026
a207503
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 26, 2026
0f810e8
Fix testing
RubenCerna2079 Feb 26, 2026
d1f6d60
Merge remote-tracking branch 'refs/remotes/origin/dev/rubencerna/crea…
RubenCerna2079 Feb 26, 2026
5b71323
Merge branch 'main' into dev/rubencerna/create-inmemory-entities-from…
RubenCerna2079 Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Cli.Tests/ConfigGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public void TestSpecialCharactersInConnectionString()
""mode"": ""production""
}
},
""autoentities"": {},
""entities"": {}
}");

Expand Down
2 changes: 2 additions & 0 deletions src/Cli.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ public static void Init()
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.FeatureFlags);
// Ignore the JSON schema path as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.Schema);
// Ignore the JSON schema path as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.Autoentities);
// Ignore the message as that's not serialized in our config file anyway.
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.DatabaseTypeNotSupportedMessage);
// Ignore DefaultDataSourceName as that's not serialized in our config file.
Expand Down
1 change: 1 addition & 0 deletions src/Cli.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,7 @@ public static string GenerateConfigWithGivenDepthLimit(string? depthLimitJson =
}}
}}
}},
""autoentities"": {{}},
""entities"": {{}}";

return $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{runtimeSection}}}";
Expand Down
2 changes: 1 addition & 1 deletion src/Config/Converters/RuntimeAutoentitiesConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class RuntimeAutoentitiesConverter : JsonConverter<RuntimeAutoentities>
public override void Write(Utf8JsonWriter writer, RuntimeAutoentities value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach ((string key, Autoentity autoEntity) in value.AutoEntities)
foreach ((string key, Autoentity autoEntity) in value.Autoentities)
{
writer.WritePropertyName(key);
JsonSerializer.Serialize(writer, autoEntity, options);
Expand Down
21 changes: 16 additions & 5 deletions src/Config/ObjectModel/RuntimeAutoentities.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.Converters;

Expand All @@ -10,19 +11,29 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// Represents a collection of <see cref="Autoentity"/> available from the RuntimeConfig.
/// </summary>
[JsonConverter(typeof(RuntimeAutoentitiesConverter))]
public record RuntimeAutoentities
public record RuntimeAutoentities : IEnumerable<KeyValuePair<string, Autoentity>>
{
/// <summary>
/// The collection of <see cref="Autoentity"/> available from the RuntimeConfig.
/// </summary>
public IReadOnlyDictionary<string, Autoentity> AutoEntities { get; init; }
public IReadOnlyDictionary<string, Autoentity> Autoentities { get; init; }

/// <summary>
/// Creates a new instance of the <see cref="RuntimeAutoentities"/> class using a collection of entities.
/// </summary>
/// <param name="autoEntities">The collection of auto-entities to map to RuntimeAutoentities.</param>
public RuntimeAutoentities(IReadOnlyDictionary<string, Autoentity> autoEntities)
/// <param name="autoentities">The collection of auto-entities to map to RuntimeAutoentities.</param>
public RuntimeAutoentities(IReadOnlyDictionary<string, Autoentity> autoentities)
{
AutoEntities = autoEntities;
Autoentities = autoentities;
}

public IEnumerator<KeyValuePair<string, Autoentity>> GetEnumerator()
{
return Autoentities.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
75 changes: 64 additions & 11 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public record RuntimeConfig
[JsonPropertyName("azure-key-vault")]
public AzureKeyVaultOptions? AzureKeyVault { get; init; }

public RuntimeAutoentities? Autoentities { get; init; }
public RuntimeAutoentities Autoentities { get; init; }

public virtual RuntimeEntities Entities { get; init; }

Expand Down Expand Up @@ -216,6 +216,8 @@ Runtime.GraphQL.FeatureFlags is not null &&

private Dictionary<string, string> _entityNameToDataSourceName = new();

private Dictionary<string, string> _autoentityNameToDataSourceName = new();

private Dictionary<string, string> _entityPathNameToEntityName = new();

/// <summary>
Expand Down Expand Up @@ -245,6 +247,21 @@ public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)]
return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName);
}

public bool TryAddEntityNameToDataSourceName(string entityName)
{
return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName);
}

public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, string autoEntityDefinition)
{
if (_autoentityNameToDataSourceName.TryGetValue(autoEntityDefinition, out string? dataSourceName))
{
return _entityNameToDataSourceName.TryAdd(entityName, dataSourceName);
}

return false;
}

/// <summary>
/// Constructor for runtimeConfig.
/// To be used when setting up from cli json scenario.
Expand All @@ -268,8 +285,8 @@ public RuntimeConfig(
this.DataSource = DataSource;
this.Runtime = Runtime;
this.AzureKeyVault = AzureKeyVault;
this.Entities = Entities;
this.Autoentities = Autoentities;
this.Entities = Entities ?? new RuntimeEntities(new Dictionary<string, Entity>());
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>());
this.DefaultDataSourceName = Guid.NewGuid().ToString();

if (this.DataSource is null)
Expand All @@ -287,25 +304,38 @@ public RuntimeConfig(
};

_entityNameToDataSourceName = new Dictionary<string, string>();
if (Entities is null)
if (Entities is null && this.Entities.Entities.Count == 0 &&
Autoentities is null && this.Autoentities.Autoentities.Count == 0)
{
throw new DataApiBuilderException(
message: "entities is a mandatory property in DAB Config",
message: "Configuration file should contain either at least the entities or autoentities property",
statusCode: HttpStatusCode.UnprocessableEntity,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
}

foreach (KeyValuePair<string, Entity> entity in Entities)
if (Entities is not null)
{
foreach (KeyValuePair<string, Entity> entity in Entities)
{
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
}
}

if (Autoentities is not null)
{
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
foreach (KeyValuePair<string, Autoentity> autoentity in Autoentities)
{
_autoentityNameToDataSourceName.TryAdd(autoentity.Key, this.DefaultDataSourceName);
}
}

// Process data source and entities information for each database in multiple database scenario.
this.DataSourceFiles = DataSourceFiles;

if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null)
{
IEnumerable<KeyValuePair<string, Entity>> allEntities = Entities.AsEnumerable();
IEnumerable<KeyValuePair<string, Entity>>? allEntities = Entities?.AsEnumerable();
IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = Autoentities?.AsEnumerable();
// 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.
Expand All @@ -322,7 +352,9 @@ 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);
allEntities = allEntities.Concat(config.Entities.AsEnumerable());
_autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
allEntities = allEntities?.Concat(config.Entities.AsEnumerable());
allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable());
}
catch (Exception e)
{
Expand All @@ -336,7 +368,8 @@ public RuntimeConfig(
}
}

this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value));
this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Entity>());
this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Autoentity>());
}

SetupDataSourcesUsed();
Expand All @@ -351,17 +384,19 @@ public RuntimeConfig(
/// <param name="DataSource">Default datasource.</param>
/// <param name="Runtime">Runtime settings.</param>
/// <param name="Entities">Entities</param>
/// <param name="Autoentities">Autoentities</param>
/// <param name="DataSourceFiles">List of datasource files for multiple db scenario.Null for single db scenario.
/// <param name="DefaultDataSourceName">DefaultDataSourceName to maintain backward compatibility.</param>
/// <param name="DataSourceNameToDataSource">Dictionary mapping datasourceName to datasource object.</param>
/// <param name="EntityNameToDataSourceName">Dictionary mapping entityName to datasourceName.</param>
/// <param name="DataSourceFiles">Datasource files which represent list of child runtimeconfigs for multi-db scenario.</param>
public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary<string, DataSource> DataSourceNameToDataSource, Dictionary<string, string> EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null)
public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary<string, DataSource> DataSourceNameToDataSource, Dictionary<string, string> EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null, RuntimeAutoentities? Autoentities = null)
{
this.Schema = Schema;
this.DataSource = DataSource;
this.Runtime = Runtime;
this.Entities = Entities;
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>());
this.DefaultDataSourceName = DefaultDataSourceName;
_dataSourceNameToDataSource = DataSourceNameToDataSource;
_entityNameToDataSourceName = EntityNameToDataSourceName;
Expand Down Expand Up @@ -451,6 +486,24 @@ public DataSource GetDataSourceFromEntityName(string entityName)
return _dataSourceNameToDataSource[_entityNameToDataSourceName[entityName]];
}

/// <summary>
/// Gets datasourceName from AutoentityNameToDatasourceName dictionary.
/// </summary>
/// <param name="autoentityName">autoentityName</param>
/// <returns>DataSourceName</returns>
public string GetDataSourceNameFromAutoentityName(string autoentityName)
{
if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource))
{
throw new DataApiBuilderException(
message: $"{autoentityName} is not a valid autoentity.",
statusCode: HttpStatusCode.NotFound,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

return autoentityDataSource;
}

/// <summary>
/// Validates if datasource is present in runtimeConfig.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,4 +499,9 @@ public void InsertWantedChangesInProductionMode()
RuntimeConfig = runtimeConfigCopy;
}
}

public void EditRuntimeConfig(RuntimeConfig newRuntimeConfig)
{
RuntimeConfig = newRuntimeConfig;
}
}
15 changes: 15 additions & 0 deletions src/Core/Configurations/RuntimeConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,19 @@ private static RuntimeConfig HandleCosmosNoSqlConfiguration(string? schema, Runt

return runtimeConfig;
}

public void AddMergedEntitiesToConfig(Dictionary<string, Entity> newEntities)
{
Dictionary<string, Entity> entities = new(_configLoader.RuntimeConfig!.Entities);
foreach ((string name, Entity entity) in newEntities)
{
entities.Add(name, entity);
}

RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
{
Entities = new(entities)
};
_configLoader.EditRuntimeConfig(newRuntimeConfig);
}
}
88 changes: 86 additions & 2 deletions src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,93 @@ private bool TryResolveDbType(string sqlDbTypeName, out DbType dbType)
}

/// <inheritdoc/>
protected override async Task GenerateAutoentitiesIntoEntities()
protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary<string, Autoentity>? autoentities)
{
await Task.CompletedTask;
if (autoentities is null)
{
return;
}

RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
Dictionary<string, Entity> entities = new();
foreach ((string autoentityName, Autoentity autoentity) in autoentities)
{
int addedEntities = 0;
JsonArray? resultArray = await QueryAutoentitiesAsync(autoentity);
if (resultArray is null)
{
continue;
}

foreach (JsonObject? resultObject in resultArray)
{
if (resultObject is null)
{
throw new DataApiBuilderException(
message: $"Cannot create new entity from autoentity pattern due to an internal error.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}

// Extract the entity name, schema, and database object name from the query result.
// The SQL query returns these values with placeholders already replaced.
string? entityName = resultObject["entity_name"]?.ToString();
string? objectName = resultObject["object"]?.ToString();
string? schemaName = resultObject["schema"]?.ToString();

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);
continue;
}

// Create the entity using the template settings and permissions from the autoentity configuration.
// Currently the source type is always Table for auto-generated entities from database objects.
Entity generatedEntity = new(
Source: new EntitySource(
Object: objectName,
Type: EntitySourceType.Table,
Parameters: null,
KeyFields: null),
GraphQL: autoentity.Template.GraphQL,
Rest: autoentity.Template.Rest,
Mcp: autoentity.Template.Mcp,
Permissions: autoentity.Permissions,
Cache: autoentity.Template.Cache,
Health: autoentity.Template.Health,
Fields: null,
Relationships: null,
Mappings: new());

// Add the generated entity to the linking entities dictionary.
// This allows the entity to be processed later during metadata population.
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}'.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}

if (runtimeConfig.IsRestEnabled)
{
_logger.LogInformation("[{entity}] REST path: {globalRestPath}/{entityRestPath}", entityName, runtimeConfig.RestPath, entityName);
}
else
{
_logger.LogInformation(message: "REST calls are disabled for the entity: {entity}", entityName);
}

addedEntities++;
}

if (addedEntities == 0)
{
_logger.LogWarning($"No new entities were generated from the autoentity {autoentityName} defined in the configuration.");
}
}

_runtimeConfigProvider.AddMergedEntitiesToConfig(entities);
}

public async Task<JsonArray?> QueryAutoentitiesAsync(Autoentity autoentity)
Expand Down
Loading