From 528f42d6f6d492cfd4a22b067b7a7661a7149b62 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 16 Dec 2024 13:40:04 +0100 Subject: [PATCH 1/2] Support edit and refresh with CLI config file Treat .dacpac as more equal to .sqlproj --- .../EFCorePowerToolsPackage.cs | 33 ++- src/GUI/RevEng.Shared/Providers.cs | 27 ++ .../Shared/Extensions/ProjectExtensions.cs | 20 +- src/GUI/Shared/Handlers/CliHandler.cs | 245 ++++++++++++++++++ src/GUI/Shared/Helpers/SqlProjHelper.cs | 28 +- src/GUI/Shared/Shared.projitems | 1 + 6 files changed, 328 insertions(+), 26 deletions(-) create mode 100644 src/GUI/Shared/Handlers/CliHandler.cs diff --git a/src/GUI/EFCorePowerTools/EFCorePowerToolsPackage.cs b/src/GUI/EFCorePowerTools/EFCorePowerToolsPackage.cs index 45664fcc3..83688414c 100644 --- a/src/GUI/EFCorePowerTools/EFCorePowerToolsPackage.cs +++ b/src/GUI/EFCorePowerTools/EFCorePowerToolsPackage.cs @@ -61,6 +61,7 @@ public sealed class EFCorePowerToolsPackage : AsyncPackage private readonly DacpacAnalyzerHandler dacpacAnalyzerHandler; private readonly DabBuilderHandler dabBuilderHandler; private readonly ErDiagramHandler erDiagramHandler; + private readonly CliHandler cliHandler; private IServiceProvider extensionServices; public EFCorePowerToolsPackage() @@ -74,6 +75,7 @@ public EFCorePowerToolsPackage() dacpacAnalyzerHandler = new DacpacAnalyzerHandler(this); dabBuilderHandler = new DabBuilderHandler(this); erDiagramHandler = new ErDiagramHandler(this); + cliHandler = new CliHandler(this); } internal EnvDTE80.DTE2 Dte2() @@ -331,8 +333,15 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke private static bool IsConfigFile(string itemName) { return itemName != null - && itemName.StartsWith("efpt.", StringComparison.OrdinalIgnoreCase) - && itemName.EndsWith(".config.json", StringComparison.OrdinalIgnoreCase); + && ((itemName.StartsWith("efpt.", StringComparison.OrdinalIgnoreCase) + && itemName.EndsWith(".config.json", StringComparison.OrdinalIgnoreCase)) + || itemName.Equals("efcpt-config.json", StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsCliConfigFile(string itemName) + { + return itemName != null + && itemName.Equals("efcpt-config.json", StringComparison.OrdinalIgnoreCase); } private static bool IsDabConfigFile(string itemName) @@ -670,7 +679,14 @@ private async void OnReverseEngineerConfigFileMenuInvokeHandler(object sender, E return; } - await reverseEngineerHandler.ReverseEngineerCodeFirstAsync(project, filename, false); + if (IsCliConfigFile(item.Text)) + { + await cliHandler.EditConfigAsync(project); + } + else + { + await reverseEngineerHandler.ReverseEngineerCodeFirstAsync(project, filename, false); + } } else if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidReverseEngineerRefresh) { @@ -679,7 +695,14 @@ private async void OnReverseEngineerConfigFileMenuInvokeHandler(object sender, E return; } - await reverseEngineerHandler.ReverseEngineerCodeFirstAsync(project, filename, true); + if (IsCliConfigFile(item.Text)) + { + await cliHandler.RunCliAsync(project); + } + else + { + await reverseEngineerHandler.ReverseEngineerCodeFirstAsync(project, filename, true); + } } else if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidDabStart) { @@ -805,7 +828,7 @@ private async void OnProjectContextMenuInvokeHandler(object sender, EventArgs e) if (await project.IsMsBuildSqlProjOrMsBuildSqlProjectAsync()) { - connectionName = await project.GetOutPutAssemblyPathAsync(); + connectionName = await project.GetDacpacPathAsync(); } } diff --git a/src/GUI/RevEng.Shared/Providers.cs b/src/GUI/RevEng.Shared/Providers.cs index ee03494e1..c7fa42a5b 100644 --- a/src/GUI/RevEng.Shared/Providers.cs +++ b/src/GUI/RevEng.Shared/Providers.cs @@ -49,6 +49,33 @@ public static DatabaseType ToDatabaseType(this string providerAlias, bool isDacp } } + public static string ToDatabaseShortName(this DatabaseType databaseType) + { + switch (databaseType) + { + case DatabaseType.Undefined: + return "Undefined"; + case DatabaseType.SQLServer: + return "mssql"; + case DatabaseType.SQLite: + return "sqlite"; + case DatabaseType.Npgsql: + return "npgsql"; + case DatabaseType.Mysql: + return "mysql"; + case DatabaseType.Oracle: + return "oracle"; + case DatabaseType.SQLServerDacpac: + return "mssql"; + case DatabaseType.Firebird: + return "firebird"; + case DatabaseType.Snowflake: + return "snowflake"; + default: + return "Undefined"; + } + } + public static HashSet GetDabProviders() { return new HashSet diff --git a/src/GUI/Shared/Extensions/ProjectExtensions.cs b/src/GUI/Shared/Extensions/ProjectExtensions.cs index 8145632cc..8d6ddbe57 100644 --- a/src/GUI/Shared/Extensions/ProjectExtensions.cs +++ b/src/GUI/Shared/Extensions/ProjectExtensions.cs @@ -46,6 +46,20 @@ public static async Task GetStartupProjectOutputPathAsync() } } + public static async Task GetDacpacPathAsync(this Project project) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var assemblyName = await project.GetAttributeAsync("SqlTargetPath"); + + if (string.IsNullOrEmpty(assemblyName)) + { + assemblyName = await project.GetAttributeAsync("TargetPath"); + } + + return assemblyName; + } + public static async Task GetOutPutAssemblyPathAsync(this Project project) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -54,7 +68,6 @@ public static async Task GetOutPutAssemblyPathAsync(this Project project var assemblyNameExe = assemblyName + ".exe"; var assemblyNameDll = assemblyName + ".dll"; - var assemblyNameDacpac = assemblyName + ".dacpac"; var outputPath = await GetOutputPathAsync(project); @@ -73,11 +86,6 @@ public static async Task GetOutPutAssemblyPathAsync(this Project project return Path.Combine(outputPath, assemblyNameDll); } - if (File.Exists(Path.Combine(outputPath, assemblyNameDacpac))) - { - return Path.Combine(outputPath, assemblyNameDacpac); - } - return null; } diff --git a/src/GUI/Shared/Handlers/CliHandler.cs b/src/GUI/Shared/Handlers/CliHandler.cs new file mode 100644 index 000000000..e864429dd --- /dev/null +++ b/src/GUI/Shared/Handlers/CliHandler.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Community.VisualStudio.Toolkit; +using EFCorePowerTools.Common.Models; +using EFCorePowerTools.Contracts.ViewModels; +using EFCorePowerTools.Contracts.Views; +using EFCorePowerTools.Extensions; +using EFCorePowerTools.Helpers; +using EFCorePowerTools.Locales; +using Microsoft.VisualStudio.Shell; +using RevEng.Common; +using RevEng.Common.Dab; + +namespace EFCorePowerTools.Handlers.ReverseEngineer +{ + internal class CliHandler + { + private readonly EFCorePowerToolsPackage package; + private readonly VsDataHelper vsDataHelper; + + public CliHandler(EFCorePowerToolsPackage package) + { + this.package = package; + vsDataHelper = new VsDataHelper(); + } + + public async Task EditConfigAsync(Project project) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + try + { + var optionsPath = Path.Combine(Path.GetDirectoryName(project.FullPath), "efcpt-config.json"); + if (!File.Exists(optionsPath)) + { + return; + } + + await VS.Documents.OpenAsync(optionsPath); + + Telemetry.TrackEvent("PowerTools.CliEdit"); + } + catch (AggregateException ae) + { + foreach (var innerException in ae.Flatten().InnerExceptions) + { + package.LogError(new List(), innerException); + } + } + catch (Exception exception) + { + package.LogError(new List(), exception); + } + } + + public async Task RunCliAsync(Project project) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + try + { + if (await VSHelper.IsDebugModeAsync()) + { + VSHelper.ShowError(ReverseEngineerLocale.CannotGenerateCodeWhileDebugging); + return; + } + + var projectPath = Path.GetDirectoryName(project.FullPath); + + var optionsPath = Path.Combine(projectPath, "efcpt-config.json"); + + if (!File.Exists(optionsPath)) + { + return; + } + + var userOptions = ReverseEngineerUserOptionsExtensions.TryRead(optionsPath, projectPath); + + if (userOptions == null) + { + userOptions = new ReverseEngineerUserOptions(); + } + + var options = new DataApiBuilderOptions(); + + DatabaseConnectionModel dbInfo = null; + + if (!await ChooseDataBaseConnectionAsync(options, userOptions)) + { + await VS.StatusBar.ClearAsync(); + return; + } + + await VS.StatusBar.ShowMessageAsync(ReverseEngineerLocale.GettingReadyToConnect); + + dbInfo = await GetDatabaseInfoAsync(options); + + if (dbInfo == null) + { + await VS.StatusBar.ClearAsync(); + return; + } + + SaveOptions(project, optionsPath, userOptions); + + LaunchCli(optionsPath, dbInfo); + + Telemetry.TrackEvent("PowerTools.CliRefresh"); + } + catch (AggregateException ae) + { + foreach (var innerException in ae.Flatten().InnerExceptions) + { + package.LogError(new List(), innerException); + } + } + catch (Exception exception) + { + package.LogError(new List(), exception); + } + } + + private async Task ChooseDataBaseConnectionAsync(DataApiBuilderOptions options, ReverseEngineerUserOptions userOptions) + { + var databaseList = await vsDataHelper.GetDataConnectionsAsync(package); + + databaseList = databaseList.Where(databaseList => Providers.GetDabProviders().Contains(databaseList.Value.DatabaseType)) + .ToDictionary(databaseList => databaseList.Key, databaseList => databaseList.Value); + + var dacpacList = await SqlProjHelper.GetDacpacFilesInActiveSolutionAsync(); + + var psd = package.GetView(); + + if (databaseList.Any()) + { + psd.PublishConnections(databaseList.Select(m => new DatabaseConnectionModel + { + ConnectionName = m.Value.ConnectionName, + ConnectionString = m.Value.ConnectionString, + DatabaseType = m.Value.DatabaseType, + DataConnection = m.Value.DataConnection, + })); + } + + if (dacpacList != null && dacpacList.Any()) + { + psd.PublishDefinitions(dacpacList.Select(m => new DatabaseConnectionModel + { + FilePath = m, + DatabaseType = DatabaseType.SQLServerDacpac, + })); + } + + psd.PublishCodeGenerationMode(CodeGenerationMode.EFCore6, new List + { + new CodeGenerationItem { Key = (int)CodeGenerationMode.EFCore8, Value = "DAB" }, + }); + + if (!string.IsNullOrEmpty(userOptions.UiHint)) + { + psd.PublishUiHint(userOptions.UiHint); + } + + psd.PublishSchemas(new List()); + + var pickDataSourceResult = psd.ShowAndAwaitUserResponse(true); + if (!pickDataSourceResult.ClosedByOK) + { + return false; + } + + options.Dacpac = pickDataSourceResult.Payload.Connection?.FilePath; + userOptions.UiHint = pickDataSourceResult.Payload.UiHint; + + if (pickDataSourceResult.Payload.Connection != null) + { + options.ConnectionString = pickDataSourceResult.Payload.Connection.ConnectionString; + options.DatabaseType = pickDataSourceResult.Payload.Connection.DatabaseType; + } + + return true; + } + + private async Task GetDatabaseInfoAsync(DataApiBuilderOptions options) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dbInfo = new DatabaseConnectionModel(); + + if (!string.IsNullOrEmpty(options.ConnectionString)) + { + dbInfo.ConnectionString = options.ConnectionString; + dbInfo.DatabaseType = options.DatabaseType; + } + + if (!string.IsNullOrEmpty(options.Dacpac)) + { + dbInfo.DatabaseType = DatabaseType.SQLServerDacpac; + dbInfo.ConnectionString = $"Data Source=(local);Initial Catalog={Path.GetFileNameWithoutExtension(options.Dacpac)};Integrated Security=true;"; + options.ConnectionString = dbInfo.ConnectionString; + options.DatabaseType = dbInfo.DatabaseType; + + options.Dacpac = await SqlProjHelper.BuildSqlProjAsync(options.Dacpac); + if (string.IsNullOrEmpty(options.Dacpac)) + { + VSHelper.ShowMessage(ReverseEngineerLocale.UnableToBuildSelectedDatabaseProject); + return null; + } + + dbInfo.FilePath = options.Dacpac; + } + + if (dbInfo.DatabaseType == DatabaseType.Undefined) + { + VSHelper.ShowError($"{ReverseEngineerLocale.UnsupportedProvider}"); + return null; + } + + return dbInfo; + } + + private void SaveOptions(Project project, string optionsPath, ReverseEngineerUserOptions userOptions) + { + if (userOptions != null && !string.IsNullOrEmpty(userOptions.UiHint)) + { + File.WriteAllText(optionsPath + ".user", userOptions.Write(Path.GetDirectoryName(project.FullPath)), Encoding.UTF8); + } + } + + private void LaunchCli(string configPath, DatabaseConnectionModel database) + { + var path = Path.GetDirectoryName(configPath); + + var proc = new Process(); + proc.StartInfo.FileName = "cmd"; + proc.StartInfo.Arguments = $" /k \"cd /d {path} && efcpt \"{database.FilePath ?? database.ConnectionString}\" {database.DatabaseType.ToDatabaseShortName()}\""; + proc.Start(); + } + } +} diff --git a/src/GUI/Shared/Helpers/SqlProjHelper.cs b/src/GUI/Shared/Helpers/SqlProjHelper.cs index 15ac20507..82aca0eb2 100644 --- a/src/GUI/Shared/Helpers/SqlProjHelper.cs +++ b/src/GUI/Shared/Helpers/SqlProjHelper.cs @@ -19,7 +19,7 @@ public static string SetRelativePathForSqlProj(string uiHint, string projectDire uiHint = PathExtensions.GetRelativePath(projectDirectory, uiHint); } - if (Path.IsPathRooted(uiHint) || uiHint.EndsWith(".dacpac", StringComparison.OrdinalIgnoreCase)) + if (Path.IsPathRooted(uiHint)) { return null; } @@ -34,7 +34,8 @@ public static string GetFullPathForSqlProj(string uiHint, string projectDirector return uiHint; } - if (uiHint.EndsWith(".sqlproj", System.StringComparison.OrdinalIgnoreCase)) + if (uiHint.EndsWith(".sqlproj", StringComparison.OrdinalIgnoreCase) + || uiHint.EndsWith(".dacpac", StringComparison.OrdinalIgnoreCase)) { return PathHelper.GetAbsPath(uiHint, projectDirectory); } @@ -66,7 +67,12 @@ public static async Task GetDacpacFilesInActiveSolutionAsync() if (await item.IsMsBuildSqlProjOrMsBuildSqlProjectAsync()) { - AddFiles(result, Path.GetDirectoryName(item.FullPath), "*.dacpac"); + var dacpacPath = await item.GetDacpacPathAsync(); + + if (!string.IsNullOrEmpty(dacpacPath)) + { + result.Add(dacpacPath); + } } try @@ -105,7 +111,7 @@ public static async Task BuildSqlProjAsync(string sqlprojPath) return null; } - var assemblyPath = await project.GetOutPutAssemblyPathAsync(); + var assemblyPath = await project.GetDacpacPathAsync(); var searchPath = Path.GetDirectoryName(assemblyPath); @@ -142,16 +148,6 @@ public static async Task BuildSqlProjAsync(string sqlprojPath) throw new InvalidOperationException("Dacpac build failed, please pick the file manually"); } - private static void AddFiles(HashSet result, string path, string pattern) - { - var searchPath = Path.Combine(path, "bin"); - - foreach (var file in Directory.GetFiles(searchPath, pattern, SearchOption.AllDirectories)) - { - result.Add(file); - } - } - private static async System.Threading.Tasks.Task LinkedFilesSearchAsync(IEnumerable projectItems, HashSet files) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -168,7 +164,9 @@ private static async System.Threading.Tasks.Task LinkedFilesSearchAsync(IEnumera var file = item as PhysicalFile; var fullPath = file.FullPath; - if (file.Extension == ".dacpac" && !string.IsNullOrEmpty(fullPath)) + if (file.Extension == ".dacpac" + && !string.IsNullOrEmpty(fullPath) + && !fullPath.StartsWith(Path.GetDirectoryName(item.FullPath), StringComparison.OrdinalIgnoreCase)) { files.Add(fullPath); } diff --git a/src/GUI/Shared/Shared.projitems b/src/GUI/Shared/Shared.projitems index 4544fe47e..b4783fc73 100644 --- a/src/GUI/Shared/Shared.projitems +++ b/src/GUI/Shared/Shared.projitems @@ -71,6 +71,7 @@ + From f71d558c1c8c38fb0e18b39f93f8443b1771ce02 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 16 Dec 2024 14:09:59 +0100 Subject: [PATCH 2/2] Simplyfy build --- src/GUI/Shared/Helpers/SqlProjHelper.cs | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/GUI/Shared/Helpers/SqlProjHelper.cs b/src/GUI/Shared/Helpers/SqlProjHelper.cs index 82aca0eb2..8fa8c7fdc 100644 --- a/src/GUI/Shared/Helpers/SqlProjHelper.cs +++ b/src/GUI/Shared/Helpers/SqlProjHelper.cs @@ -111,15 +111,6 @@ public static async Task BuildSqlProjAsync(string sqlprojPath) return null; } - var assemblyPath = await project.GetDacpacPathAsync(); - - var searchPath = Path.GetDirectoryName(assemblyPath); - - if (string.IsNullOrEmpty(searchPath)) - { - searchPath = Path.Combine(Path.GetDirectoryName(project.FullPath), "bin"); - } - if (!await VS.Build.ProjectIsUpToDateAsync(project)) { var ok = await VS.Build.BuildProjectAsync(project, BuildAction.Rebuild); @@ -130,19 +121,11 @@ public static async Task BuildSqlProjAsync(string sqlprojPath) } } - if (!Directory.Exists(searchPath)) - { - return null; - } - - var files = Directory.GetFiles(searchPath, "*.dacpac", SearchOption.AllDirectories) - .Where(f => !f.EndsWith("\\msdb.dacpac", StringComparison.OrdinalIgnoreCase) - && !f.EndsWith("\\master.dacpac", StringComparison.OrdinalIgnoreCase)) - .ToList(); + var dacpacPath = await project.GetDacpacPathAsync(); - if (files.Count == 1) + if (!string.IsNullOrEmpty(dacpacPath)) { - return files[0]; + return dacpacPath; } throw new InvalidOperationException("Dacpac build failed, please pick the file manually");