diff --git a/.github/workflows/vsix.yml b/.github/workflows/vsix.yml
index a273d9a46..5e131b076 100644
--- a/.github/workflows/vsix.yml
+++ b/.github/workflows/vsix.yml
@@ -58,7 +58,7 @@ jobs:
mkdir vsix
7z x src/GUI/lib/efreveng80.exe.zip -oefreveng80 -y
dir /a:-d /s /b "efreveng80" | find /c ":\" > filecount.txt
- findstr "157" filecount.txt
+ findstr "166" filecount.txt
- name: Extract and verify efreveng90.exe.zip file count
if: github.event_name != 'pull_request'
@@ -67,7 +67,7 @@ jobs:
mkdir vsix
7z x src/GUI/lib/efreveng90.exe.zip -oefreveng90 -y
dir /a:-d /s /b "efreveng90" | find /c ":\" > filecount.txt
- findstr "156" filecount.txt
+ findstr "165" filecount.txt
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
diff --git a/src/Core/RevEng.Core.80/DataverseModelFactoryExtension.cs b/src/Core/RevEng.Core.80/DataverseModelFactoryExtension.cs
new file mode 100644
index 000000000..216d8a299
--- /dev/null
+++ b/src/Core/RevEng.Core.80/DataverseModelFactoryExtension.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
+using Microsoft.PowerPlatform.Dataverse.Client;
+using Microsoft.Xrm.Sdk.Messages;
+using Microsoft.Xrm.Sdk.Metadata;
+using Microsoft.Xrm.Sdk.Metadata.Query;
+
+namespace RevEng.Core
+{
+ public class DataverseModelFactoryExtension
+ {
+ private readonly ServiceClient serviceClient;
+
+ public DataverseModelFactoryExtension(ServiceClient serviceClient)
+ {
+ this.serviceClient = serviceClient;
+ }
+
+ ///
+ /// Checks if the connection is for a Dataverse instance and creates a new
+ /// if so.
+ ///
+ /// The database connection to use to connect to Dataverse.
+ /// Set to a new if the represents a connection to a Dataverse TDS Endpoint.
+ /// if the represents a connection to a Dataverse TDS Endpoint, or otherwise.
+ public static bool TryCreate(DbConnection connection, out DataverseModelFactoryExtension dataverse)
+ {
+ ArgumentNullException.ThrowIfNull(connection);
+
+ var connectionStringParser = new SqlConnectionStringBuilder(connection.ConnectionString);
+ if (connection is SqlConnection sqlConnection &&
+ connectionStringParser.DataSource.Contains("dynamics.com", StringComparison.OrdinalIgnoreCase))
+ {
+ connectionStringParser.Authentication = SqlAuthenticationMethod.NotSpecified;
+ connection.ConnectionString = connectionStringParser.ToString();
+ var serviceClient = new ServiceClient($"AuthType=OAuth;Username={connectionStringParser.UserID};Url=https://{connectionStringParser.DataSource};AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto");
+ sqlConnection.AccessTokenCallback = (_, __) => Task.FromResult(new SqlAuthenticationToken(serviceClient.CurrentAccessToken, DateTimeOffset.MaxValue));
+
+ dataverse = new DataverseModelFactoryExtension(serviceClient);
+ return true;
+ }
+
+ dataverse = null;
+ return false;
+ }
+
+ ///
+ /// Updates the with metadata from the Dataverse instance.
+ ///
+ /// The table definitions that have already been loaded from the Dataverse TDS Endpoint.
+ public void GetDataverseMetadata(List tables)
+ {
+ var metadataQuery = new EntityQueryExpression
+ {
+ Properties = new MetadataPropertiesExpression(
+ nameof(EntityMetadata.LogicalName),
+ nameof(EntityMetadata.SchemaName),
+ nameof(EntityMetadata.PrimaryIdAttribute),
+ nameof(EntityMetadata.Attributes),
+ nameof(EntityMetadata.ManyToOneRelationships),
+ nameof(EntityMetadata.Keys)),
+ AttributeQuery = new AttributeQueryExpression
+ {
+ Properties = new MetadataPropertiesExpression(
+ nameof(AttributeMetadata.LogicalName),
+ nameof(AttributeMetadata.SchemaName)),
+ },
+ RelationshipQuery = new RelationshipQueryExpression
+ {
+ Properties = new MetadataPropertiesExpression(
+ nameof(OneToManyRelationshipMetadata.SchemaName),
+ nameof(OneToManyRelationshipMetadata.ReferencingAttribute),
+ nameof(OneToManyRelationshipMetadata.ReferencedEntity),
+ nameof(OneToManyRelationshipMetadata.ReferencedAttribute),
+ nameof(OneToManyRelationshipMetadata.CascadeConfiguration)),
+ },
+ KeyQuery = new EntityKeyQueryExpression
+ {
+ Properties = new MetadataPropertiesExpression(
+ nameof(EntityKeyMetadata.SchemaName),
+ nameof(EntityKeyMetadata.KeyAttributes)),
+ },
+ };
+ var metadata = (RetrieveMetadataChangesResponse)serviceClient.Execute(new RetrieveMetadataChangesRequest { Query = metadataQuery });
+
+ foreach (var entity in metadata.EntityMetadata)
+ {
+ // Check if the entity is in the table list
+ var table = tables.SingleOrDefault(t => t.Name == entity.LogicalName);
+ if (table is null)
+ {
+ continue;
+ }
+
+ // Use the schema names for tables and columns instead of the default logical names for more standard .NET naming
+ // Only switch to the schema names if they are the same as the logical name except in a different case.
+ if (entity.SchemaName != entity.LogicalName && entity.LogicalName.Equals(entity.SchemaName, StringComparison.OrdinalIgnoreCase))
+ {
+ table.Name = entity.SchemaName;
+ }
+
+ foreach (var attr in entity.Attributes)
+ {
+ var col = table.Columns.SingleOrDefault(c => c.Name == attr.LogicalName);
+
+ if (col != null && attr.SchemaName != attr.LogicalName && attr.LogicalName.Equals(attr.SchemaName, StringComparison.OrdinalIgnoreCase))
+ {
+ col.Name = attr.SchemaName;
+ }
+ }
+
+ // Add the primary key column
+ table.PrimaryKey = new DatabasePrimaryKey
+ {
+ Table = table,
+ Name = entity.PrimaryIdAttribute,
+ Columns =
+ {
+ table.Columns.Single(c => c.Name.Equals(entity.PrimaryIdAttribute, StringComparison.OrdinalIgnoreCase)),
+ },
+ };
+
+ // Add the alternate keys
+ foreach (var key in entity.Keys)
+ {
+ var uniqueConstraint = new DatabaseUniqueConstraint
+ {
+ Table = table,
+ Name = key.SchemaName,
+ };
+
+ var hasAllColumns = true;
+
+ foreach (var attr in key.KeyAttributes)
+ {
+ var col = table.Columns.SingleOrDefault(c => c.Name.Equals(attr, StringComparison.OrdinalIgnoreCase));
+
+ if (col != null)
+ {
+ uniqueConstraint.Columns.Add(col);
+ }
+ else
+ {
+ hasAllColumns = false;
+ }
+ }
+
+ if (hasAllColumns)
+ {
+ table.UniqueConstraints.Add(uniqueConstraint);
+ }
+ }
+
+ // Add the foreign key relationships
+ foreach (var relationship in entity.ManyToOneRelationships)
+ {
+ var referencedTable = tables.SingleOrDefault(t => t.Name.Equals(relationship.ReferencedEntity, StringComparison.OrdinalIgnoreCase));
+
+ if (referencedTable is null)
+ {
+ continue;
+ }
+
+ var referencingColumn = table.Columns.SingleOrDefault(c => c.Name.Equals(relationship.ReferencingAttribute, StringComparison.OrdinalIgnoreCase));
+ var referencedColumn = referencedTable.Columns.SingleOrDefault(c => c.Name.Equals(relationship.ReferencedAttribute, StringComparison.OrdinalIgnoreCase));
+
+ if (referencingColumn is null || referencedColumn is null)
+ {
+ continue;
+ }
+
+ table.ForeignKeys.Add(new DatabaseForeignKey
+ {
+ Table = table,
+ PrincipalTable = referencedTable,
+ Name = relationship.SchemaName,
+ Columns =
+ {
+ referencingColumn,
+ },
+ PrincipalColumns =
+ {
+ referencedColumn,
+ },
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/src/Core/RevEng.Core.80/PatchedSqlServerDatabaseModelFactory.cs b/src/Core/RevEng.Core.80/PatchedSqlServerDatabaseModelFactory.cs
index 0923bd42a..0299ea040 100644
--- a/src/Core/RevEng.Core.80/PatchedSqlServerDatabaseModelFactory.cs
+++ b/src/Core/RevEng.Core.80/PatchedSqlServerDatabaseModelFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
@@ -7,6 +7,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
+using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -18,7 +19,12 @@
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
+using Microsoft.PowerPlatform.Dataverse.Client;
+using Microsoft.Xrm.Sdk.Messages;
+using Microsoft.Xrm.Sdk.Metadata;
+using Microsoft.Xrm.Sdk.Metadata.Query;
using RevEng.Common;
+using RevEng.Core;
#nullable enable
namespace Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal;
@@ -83,6 +89,7 @@ private static readonly Regex PartExtractor
private byte? _compatibilityLevel;
private EngineEdition? _engineEdition;
private string? _version;
+ private DataverseModelFactoryExtension? _dataverse;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -123,6 +130,11 @@ public DatabaseModel Create(DbConnection connection, DatabaseModelFactoryOptions
var databaseModel = new DatabaseModel();
var connectionStartedOpen = connection.State == ConnectionState.Open;
+
+ // Avoid multiple login prompts for Dataverse by authenticating once with the Dataverse service client
+ // and reusing the access token for the TDS Endpoint connection as well.
+ DataverseModelFactoryExtension.TryCreate(connection, out _dataverse);
+
if (!connectionStartedOpen)
{
connection.Open();
@@ -699,13 +711,20 @@ FROM [sys].[views] AS [v]
// This is done separately due to MARS property may be turned off
GetColumns(connection, tables, tableFilterSql, viewFilter, typeAliases, databaseCollation);
- GetIndexes(connection, tables, tableFilterSql);
+ if (_dataverse != null)
+ {
+ _dataverse.GetDataverseMetadata(tables);
+ }
+ else
+ {
+ GetIndexes(connection, tables, tableFilterSql);
- GetForeignKeys(connection, tables, tableFilterSql);
+ GetForeignKeys(connection, tables, tableFilterSql);
- if (SupportsTriggers())
- {
- GetTriggers(connection, tables, tableFilterSql);
+ if (SupportsTriggers())
+ {
+ GetTriggers(connection, tables, tableFilterSql);
+ }
}
foreach (var table in tables)
diff --git a/src/Core/RevEng.Core.80/RevEng.Core.80.csproj b/src/Core/RevEng.Core.80/RevEng.Core.80.csproj
index 8503af971..9355cd190 100644
--- a/src/Core/RevEng.Core.80/RevEng.Core.80.csproj
+++ b/src/Core/RevEng.Core.80/RevEng.Core.80.csproj
@@ -22,6 +22,7 @@
+
diff --git a/src/Core/RevEng.Core.90/RevEng.Core.90.csproj b/src/Core/RevEng.Core.90/RevEng.Core.90.csproj
index 930a44810..269f343d9 100644
--- a/src/Core/RevEng.Core.90/RevEng.Core.90.csproj
+++ b/src/Core/RevEng.Core.90/RevEng.Core.90.csproj
@@ -36,6 +36,7 @@
+