-
Notifications
You must be signed in to change notification settings - Fork 305
Create Entities in-memory from Autoentities #3129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a9a4ea0
4acee5c
67e10d7
5453cbc
8fbea75
cf4debf
88b8c66
612d4bd
a396fab
c8ed0d5
947fdec
e5ae16a
2f256e4
6fca50d
8b88815
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -245,6 +245,11 @@ public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] | |||||
| return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName); | ||||||
| } | ||||||
|
|
||||||
| public bool TryAddEntityNameToDataSourceName(string entityName) | ||||||
| { | ||||||
| return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Constructor for runtimeConfig. | ||||||
| /// To be used when setting up from cli json scenario. | ||||||
|
|
@@ -268,7 +273,7 @@ public RuntimeConfig( | |||||
| this.DataSource = DataSource; | ||||||
| this.Runtime = Runtime; | ||||||
| this.AzureKeyVault = AzureKeyVault; | ||||||
| this.Entities = Entities; | ||||||
| this.Entities = Entities ?? new RuntimeEntities(new Dictionary<string, Entity>()); | ||||||
| this.Autoentities = Autoentities; | ||||||
| this.DefaultDataSourceName = Guid.NewGuid().ToString(); | ||||||
|
|
||||||
|
|
@@ -287,25 +292,28 @@ public RuntimeConfig( | |||||
| }; | ||||||
|
|
||||||
| _entityNameToDataSourceName = new Dictionary<string, string>(); | ||||||
| if (Entities is null) | ||||||
| if ((Entities is null || Entities.Entities.Count == 0) && Autoentities is null) | ||||||
| { | ||||||
| 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", | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| statusCode: HttpStatusCode.UnprocessableEntity, | ||||||
| subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); | ||||||
| } | ||||||
|
|
||||||
| foreach (KeyValuePair<string, Entity> entity in Entities) | ||||||
| if (Entities is not null) | ||||||
| { | ||||||
| _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName); | ||||||
| foreach (KeyValuePair<string, Entity> entity in Entities) | ||||||
| { | ||||||
| _entityNameToDataSourceName.TryAdd(entity.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(); | ||||||
| // 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. | ||||||
|
|
@@ -322,7 +330,7 @@ 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()); | ||||||
| allEntities = allEntities?.Concat(config.Entities.AsEnumerable()); | ||||||
| } | ||||||
| catch (Exception e) | ||||||
| { | ||||||
|
|
@@ -336,7 +344,7 @@ 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>()); | ||||||
| } | ||||||
|
|
||||||
| SetupDataSourcesUsed(); | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -411,4 +411,13 @@ private static RuntimeConfig HandleCosmosNoSqlConfiguration(string? schema, Runt | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return runtimeConfig; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public void AddNewEntitiesToConfig(Dictionary<string, Entity> entities) | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| Entities = new(entities) | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+417
to
+420
|
||||||||||||||||||||||||||||||||||||||
| 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 | |
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -294,7 +294,86 @@ private bool TryResolveDbType(string sqlDbTypeName, out DbType dbType) | |||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||
| protected override async Task GenerateAutoentitiesIntoEntities() | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| await Task.CompletedTask; | ||||||||||||||||||||||||||||||||
| int addedEntities = 0; | ||||||||||||||||||||||||||||||||
| RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); | ||||||||||||||||||||||||||||||||
| Dictionary<string, Entity> entities = (Dictionary<string, Entity>)_entities; | ||||||||||||||||||||||||||||||||
| if (runtimeConfig.Autoentities is null) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+299
to
+304
|
||||||||||||||||||||||||||||||||
| 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
AI
Feb 13, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5231,6 +5231,148 @@ public async Task TestGraphQLIntrospectionQueriesAreNotImpactedByDepthLimit() | |
| } | ||
| } | ||
|
|
||
| [TestCategory(TestCategory.MSSQL)] | ||
| [DataTestMethod] | ||
| [DataRow(true, DisplayName = "Test Autoentities with additional entities")] | ||
| [DataRow(false, DisplayName = "Test Autoentities without additional entities")] | ||
| public async Task TestAutoentitiesAreGeneratedIntoEntities(bool useEntities) | ||
| { | ||
| // Arrange | ||
| EntityRelationship bookRelationship = new(Cardinality: Cardinality.One, | ||
| TargetEntity: "BookPublisher", | ||
| SourceFields: new string[] { }, | ||
| TargetFields: new string[] { }, | ||
| LinkingObject: null, | ||
| LinkingSourceFields: null, | ||
| LinkingTargetFields: null); | ||
|
|
||
| Entity bookEntity = new(Source: new("books", EntitySourceType.Table, null, null), | ||
| Fields: null, | ||
| Rest: null, | ||
| GraphQL: new(Singular: "book", Plural: "books"), | ||
| Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, | ||
| Relationships: new Dictionary<string, EntityRelationship>() { { "publishers", bookRelationship } }, | ||
| Mappings: null); | ||
|
|
||
| EntityRelationship publisherRelationship = new(Cardinality: Cardinality.Many, | ||
| TargetEntity: "Book", | ||
| SourceFields: new string[] { }, | ||
| TargetFields: new string[] { }, | ||
| LinkingObject: null, | ||
| LinkingSourceFields: null, | ||
| LinkingTargetFields: null); | ||
|
|
||
| Entity publisherEntity = new( | ||
| Source: new("publishers", EntitySourceType.Table, null, null), | ||
| Fields: null, | ||
| Rest: null, | ||
| GraphQL: new(Singular: "bookpublisher", Plural: "bookpublishers"), | ||
| Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, | ||
| Relationships: new Dictionary<string, EntityRelationship>() { { "books", publisherRelationship } }, | ||
| Mappings: null); | ||
|
|
||
| Dictionary<string, Entity> entityMap = new() | ||
| { | ||
| { "Book", bookEntity }, | ||
| { "BookPublisher", publisherEntity } | ||
| }; | ||
|
|
||
| Dictionary<string, Autoentity> autoentityMap = new() | ||
| { | ||
| { | ||
| "PublisherAutoEntity", new Autoentity( | ||
| Patterns: new AutoentityPatterns( | ||
| Include: new[] { "%publishers%" }, | ||
| Exclude: null, | ||
| Name: null | ||
| ), | ||
| Template: new AutoentityTemplate( | ||
| Rest: new EntityRestOptions(Enabled: true), | ||
| GraphQL: new EntityGraphQLOptions( | ||
| Singular: string.Empty, | ||
| Plural: string.Empty, | ||
| Enabled: true | ||
| ), | ||
| Health: null, | ||
| Cache: null | ||
| ), | ||
| Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) } | ||
| ) | ||
| } | ||
| }; | ||
|
|
||
| // Create DataSource for MSSQL connection | ||
| DataSource dataSource = new(DatabaseType.MSSQL, | ||
| GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); | ||
|
|
||
| // Build complete runtime configuration with autoentities | ||
| RuntimeConfig configuration = new( | ||
| Schema: "TestAutoentitiesSchema", | ||
| DataSource: dataSource, | ||
| Runtime: new( | ||
| Rest: new(Enabled: true), | ||
| GraphQL: new(Enabled: true), | ||
| Mcp: new(Enabled: false), | ||
| Host: new( | ||
| Cors: null, | ||
| Authentication: new Config.ObjectModel.AuthenticationOptions( | ||
| Provider: nameof(EasyAuthType.StaticWebApps), | ||
| Jwt: null | ||
| ) | ||
| ) | ||
| ), | ||
| Entities: new(useEntities ? entityMap : new Dictionary<string, Entity>()), | ||
| Autoentities: new RuntimeAutoentities(autoentityMap) | ||
| ); | ||
|
|
||
| File.WriteAllText(CUSTOM_CONFIG_FILENAME, configuration.ToJson()); | ||
|
|
||
| string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" }; | ||
|
|
||
| using (TestServer server = new(Program.CreateWebHostBuilder(args))) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| using (HttpClient client = server.CreateClient()) | ||
| { | ||
| // Act | ||
| HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/publishers"); | ||
|
||
| HttpResponseMessage restResponse = await client.SendAsync(restRequest); | ||
|
|
||
| string graphqlQuery = @" | ||
| { | ||
| publishers { | ||
| items { | ||
| id | ||
| name | ||
| } | ||
| } | ||
| }"; | ||
|
|
||
| object graphqlPayload = new { query = graphqlQuery }; | ||
| HttpRequestMessage graphqlRequest = new(HttpMethod.Post, "/graphql") | ||
| { | ||
| Content = JsonContent.Create(graphqlPayload) | ||
| }; | ||
|
Comment on lines
+5350
to
+5353
|
||
| HttpResponseMessage graphqlResponse = await client.SendAsync(graphqlRequest); | ||
|
|
||
| // Assert | ||
| string expectedResponseFragment = @"{""id"":1156,""name"":""The First Publisher""}"; | ||
|
|
||
| // Verify REST response | ||
| Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode, "REST request to auto-generated entity should succeed"); | ||
|
|
||
| string restResponseBody = await restResponse.Content.ReadAsStringAsync(); | ||
| Assert.IsTrue(!string.IsNullOrEmpty(restResponseBody), "REST response should contain data"); | ||
| Assert.IsTrue(restResponseBody.Contains(expectedResponseFragment)); | ||
|
|
||
| // Verify GraphQL response | ||
| Assert.AreEqual(HttpStatusCode.OK, graphqlResponse.StatusCode, "GraphQL request to auto-generated entity should succeed"); | ||
|
|
||
| string graphqlResponseBody = await graphqlResponse.Content.ReadAsStringAsync(); | ||
| Assert.IsTrue(!string.IsNullOrEmpty(graphqlResponseBody), "GraphQL response should contain data"); | ||
| Assert.IsFalse(graphqlResponseBody.Contains("errors"), "GraphQL response should not contain errors"); | ||
| Assert.IsTrue(graphqlResponseBody.Contains(expectedResponseFragment)); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
|
|
||
There was a problem hiding this comment.
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 adataSourceName(and use_dataSourceNamefrom the metadata provider) so the entity->datasource mapping stays correct.