Skip to content

Create Entities in-memory from Autoentities#3129

Open
RubenCerna2079 wants to merge 15 commits intomainfrom
dev/rubencerna/create-inmemory-entities-from-autoentities
Open

Create Entities in-memory from Autoentities#3129
RubenCerna2079 wants to merge 15 commits intomainfrom
dev/rubencerna/create-inmemory-entities-from-autoentities

Conversation

@RubenCerna2079
Copy link
Contributor

@RubenCerna2079 RubenCerna2079 commented Feb 13, 2026

Why make this change?

  • Create entities in-memory from autoentities #3052
    We need to generate all the entities from the autoentities properties. In order to do this we need to use the query that was previously created and, add the newly generated entities into the runtime so the user can use them.

What is this change?

  • MsSqlMetadataProvider.cs: Finish creating the GenerateAutoentitiesIntoEntities function so that it uses the query to receive all of the tables and turn them into entities inside the runtimeConfig.
  • RuntimeConfig.cs: Create new function that adds the new entities to the runtimeConfig. And also change the runtimeConfig to allow for the entities property to be missing if the user decides to use the autoentities property.

How was this tested?

  • Integration Tests
  • Unit Tests

@RubenCerna2079 RubenCerna2079 added this to the Feb 2026 milestone Feb 13, 2026
@RubenCerna2079 RubenCerna2079 self-assigned this Feb 13, 2026
@RubenCerna2079 RubenCerna2079 linked an issue Feb 13, 2026 that may be closed by this pull request
@RubenCerna2079 RubenCerna2079 force-pushed the dev/rubencerna/create-inmemory-entities-from-autoentities branch from 1972a8f to 2f256e4 Compare February 13, 2026 00:52
@RubenCerna2079 RubenCerna2079 marked this pull request as ready for review February 13, 2026 01:13
Copilot AI review requested due to automatic review settings February 13, 2026 01:13
@RubenCerna2079
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables auto-generating entity definitions from autoentities at runtime (in-memory) so the generated entities become usable by the REST/GraphQL runtime without being serialized into the entities section of the config.

Changes:

  • Implemented MSSQL autoentity generation to materialize matching tables into RuntimeConfig.Entities during metadata initialization.
  • Updated RuntimeConfig/validation to allow entities to be omitted when autoentities is present, and added APIs to mutate the in-memory runtime config.
  • Added an MSSQL test covering autoentities generation with/without pre-existing entities.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/Service/Startup.cs Adds TODOs related to validation placement with autoentity generation.
src/Service.Tests/Configuration/ConfigurationTests.cs Adds test coverage for autoentities generating usable REST/GraphQL endpoints.
src/Core/Services/MetadataProviders/SqlMetadataProvider.cs Exposes _entities to derived classes and triggers autoentity generation during MSSQL initialization.
src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs Implements GenerateAutoentitiesIntoEntities() and query execution for autoentities.
src/Core/Configurations/RuntimeConfigValidator.cs Allows entity validation loop to safely handle missing Entities.
src/Core/Configurations/RuntimeConfigProvider.cs Adds method to replace runtime config entities in-memory after generation.
src/Config/RuntimeConfigLoader.cs Adds a setter-style method to update the active RuntimeConfig instance.
src/Config/ObjectModel/RuntimeConfig.cs Allows missing entities when autoentities exists; adds entity->datasource mapping helper.

Comment on lines +248 to +250
public bool TryAddEntityNameToDataSourceName(string entityName)
{
return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method always maps newly added entities to DefaultDataSourceName. That will be incorrect when autoentities are generated while initializing a non-default datasource (multi-database scenario). Consider changing the API to accept a dataSourceName (and use _dataSourceName from the metadata provider) so the entity->datasource mapping stays correct.

Suggested change
public bool TryAddEntityNameToDataSourceName(string entityName)
{
return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName);
public bool TryAddEntityNameToDataSourceName(string entityName, string? dataSourceName = null)
{
string effectiveDataSourceName = string.IsNullOrEmpty(dataSourceName) ? this.DefaultDataSourceName : dataSourceName;
return _entityNameToDataSourceName.TryAdd(entityName, effectiveDataSourceName);

Copilot uses AI. Check for mistakes.
Comment on lines +417 to +420
RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
{
Entities = new(entities)
};
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddNewEntitiesToConfig replaces the entire RuntimeConfig.Entities collection with the provided dictionary. In multi-datasource scenarios (or any scenario where entities is a subset), this can unintentionally drop entities and desync internal lookup dictionaries. Prefer merging with the existing entity set (and updating the entity->datasource map consistently) rather than overwriting.

Suggested change
RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
{
Entities = new(entities)
};
// Merge incoming entities into the existing entity set instead of overwriting it.
RuntimeConfig currentRuntimeConfig = _configLoader.RuntimeConfig!;
Dictionary<string, Entity> mergedEntities = new(currentRuntimeConfig.Entities);
foreach ((string entityName, Entity entityDefinition) in entities)
{
// Add new entities or update existing ones with the provided definitions.
mergedEntities[entityName] = entityDefinition;
}
RuntimeConfig newRuntimeConfig = currentRuntimeConfig with
{
Entities = mergedEntities
};

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is right on this one. This replaces the entire entity collection. Combined with entities being sourced from _entities (which is per-datasource), this drops entities from other datasources. Merge into the existing set instead. Something like this:

var merged = new Dictionary<string, Entity>(_configLoader.RuntimeConfig!.Entities.Entities);
foreach (var kvp in entities) merged[kvp.Key] = kvp.Value;

Comment on lines +300 to +305
Dictionary<string, Entity> entities = (Dictionary<string, Entity>)_entities;
if (runtimeConfig.Autoentities is null)
{
return;
}

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entities is taken from _entities, which is a per-datasource filtered dictionary (see SqlMetadataProvider ctor). Passing that dictionary into AddNewEntitiesToConfig risks replacing RuntimeConfig.Entities with only the entities for this datasource, dropping entities for other datasources and leaving _entityNameToDataSourceName inconsistent. Use the full runtimeConfig.Entities.Entities as the base and merge in generated entities, or make AddNewEntitiesToConfig merge rather than replace.

Suggested change
Dictionary<string, Entity> entities = (Dictionary<string, Entity>)_entities;
if (runtimeConfig.Autoentities is null)
{
return;
}
if (runtimeConfig.Autoentities is null)
{
return;
}
// Start from the full set of entities defined in the runtime configuration,
// not the per-datasource filtered _entities dictionary, so that when we
// persist new auto-generated entities we don't drop entities for other datasources.
Dictionary<string, Entity> entities = new(runtimeConfig.Entities.Entities);

Copilot uses AI. Check for mistakes.
Comment on lines +318 to +335
string entityName = resultObject["entity_name"]!.ToString();
string schemaName = resultObject["schema"]!.ToString();
string objectName = resultObject["object"]!.ToString();

if (string.IsNullOrWhiteSpace(entityName) || string.IsNullOrWhiteSpace(objectName))
{
_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),
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schemaName is read from the autoentities query but not used. If a matched table is in a non-default schema, setting EntitySource.Object to just objectName will make DAB resolve it against the default schema (dbo) and point to the wrong table. Consider using a schema-qualified source (e.g., schemaName.objectName) when schemaName is not empty/default.

Copilot uses AI. Check for mistakes.
using (HttpClient client = server.CreateClient())
{
// Act
HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/publishers");
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'HttpRequestMessage' is created but not disposed.

Copilot uses AI. Check for mistakes.
Comment on lines +5351 to +5354
HttpRequestMessage graphqlRequest = new(HttpMethod.Post, "/graphql")
{
Content = JsonContent.Create(graphqlPayload)
};
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'HttpRequestMessage' is created but not disposed.

Copilot uses AI. Check for mistakes.
@RubenCerna2079
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

{
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message: "Configuration file should contain either at least the Entities or Autoentities property",
message: "Configuration file should contain either at least the entities or autoentities property",

}
else
{
_runtimeConfigProvider.AddNewEntitiesToConfig(entities);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont we have access to the RuntimeConfigLoader here?

return runtimeConfig;
}

public void AddNewEntitiesToConfig(Dictionary<string, Entity> entities)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the function name to "AddMergedEntitiesToConfig" since the entities passed in here, are all the entities - original + entities found from autoentities.


string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" };

using (TestServer server = new(Program.CreateWebHostBuilder(args)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also retrieve the serviceProvider -> RuntimeConfigProvider and the RuntimeConfig to check the entities member has 3 entities with useentity and 1 when not using previous entities?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per @Aniruddh25's feedback: after the server starts, resolve RuntimeConfigProvider from the service provider and assert the entity count. Expect 3 when useEntities=true (2 manual + 1 auto-generated) and 1 when useEntities=false. This proves the autoentity pipeline actually ran, rather than relying only on REST/GraphQL responses which could pass for the wrong reasons.

@RubenCerna2079
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

await Task.CompletedTask;
int addedEntities = 0;
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
Dictionary<string, Entity> entities = (Dictionary<string, Entity>)_entities;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not so much a technical problem as a code behavior problem. This line breaks the contract of IReadOnlyDictionary. For some reason, a developer typed this as read-only. Why? Presumably to protect it from mutation. But that developer made a mistake by assigning it via ToDictionary().

So, your cast works today because _entities is assigned via .ToDictionary() in the base class, but the declared type is IReadOnlyDictionary. If someone later CORRECTS it to ReadOnlyDictionary, this line will fail with InvalidCastException at runtime with no compile-time hint.

That's not all. _entities only has entities for this datasource (it is filtered in the SqlMetadataProvider constructor). So when you pass it to AddNewEntitiesToConfig that code will replace the full set with a subset, dropping entities from other datasources. 🫨

Suggestion: The easiest is to build a local dictionary from runtimeConfig.Entities.Entities (the full set), merge auto-generated entities into it, and pass that to AddNewEntitiesToConfig. That avoids both problems.

continue;
}

foreach (JsonObject resultObject in resultArray!)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! is unnecessary here. You already guard for null on line 308 with continue, so the compiler knows resultArray is not null at this point. Remove the ! to keep things clean. I know these are easy to just keep everywhere, but it actually can introduce hard to debug problems later.

Comment on lines +317 to +319
string entityName = resultObject["entity_name"]!.ToString();
string schemaName = resultObject["schema"]!.ToString();
string objectName = resultObject["object"]!.ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the SQL result has a row where one of these keys is missing, the ! will cause a NullReferenceException before reaching your IsNullOrWhiteSpace guard below. Use ?.ToString() and let the null check catch it:

string? entityName = resultObject["entity_name"]?.ToString();

}
}

public async Task<JsonArray?> QueryAutoentitiesAsync(Autoentity autoentity)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoentityPatterns.Exclude is string[]? in the constructor. If someone passes null, string.Join throws ArgumentNullException. Coalesce it: autoentity.Patterns.Exclude ?? Array.Empty().

Comment on lines +417 to +420
RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
{
Entities = new(entities)
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is right on this one. This replaces the entire entity collection. Combined with entities being sourced from _entities (which is per-datasource), this drops entities from other datasources. Merge into the existing set instead. Something like this:

var merged = new Dictionary<string, Entity>(_configLoader.RuntimeConfig!.Entities.Entities);
foreach (var kvp in entities) merged[kvp.Key] = kvp.Value;


_entityNameToDataSourceName = new Dictionary<string, string>();
if (Entities is null)
if ((Entities is null || Entities.Entities.Count == 0) && Autoentities is null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 276 you already set this.Entities = Entities ?? new RuntimeEntities(...), so Entities is null can never be true here. The null branch is dead code. SImplify to if (Entities.Entities.Count == 0 && Autoentities is null)

if (runtimeConfig.IsDevelopmentMode())
{
// Running only in developer mode to ensure fast and smooth startup in production.
// TODO: Add this check at the end of generating the new entities and skip this one only if it is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the mystery? "If it is..." What?! I suggest you finish the sentence and also link to the task here. It's okay to have a TODO in a PR. Just always think that you might hand this off to another developer, so don't use too much shorthand.


string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" };

using (TestServer server = new(Program.CreateWebHostBuilder(args)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per @Aniruddh25's feedback: after the server starts, resolve RuntimeConfigProvider from the service provider and assert the entity count. Expect 3 when useEntities=true (2 manual + 1 auto-generated) and 1 when useEntities=false. This proves the autoentity pipeline actually ran, rather than relying only on REST/GraphQL responses which could pass for the wrong reasons.


// Represents the entities exposed in the runtime config.
private IReadOnlyDictionary<string, Entity> _entities;
protected IReadOnlyDictionary<string, Entity> _entities;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed from private to protected so the subclass can cast and mutate it. If you take the approach of building a local dictionary from runtimeConfig.Entities instead of mutating _entities directly, you can revert this back to private and preserve encapsulation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Create entities in-memory from autoentities

3 participants