diff --git a/MsSql.ClassGenerator.Cli/Program.cs b/MsSql.ClassGenerator.Cli/Program.cs index 785fa21..11bc67c 100644 --- a/MsSql.ClassGenerator.Cli/Program.cs +++ b/MsSql.ClassGenerator.Cli/Program.cs @@ -1,11 +1,10 @@ -using System.Diagnostics; -using System.Reflection; -using MsSql.ClassGenerator.Cli.Model; +using MsSql.ClassGenerator.Cli.Model; using MsSql.ClassGenerator.Core.Business; using MsSql.ClassGenerator.Core.Common; using MsSql.ClassGenerator.Core.Model; using Serilog; -using Serilog.Events; +using System.Diagnostics; +using System.Reflection; namespace MsSql.ClassGenerator.Cli; @@ -21,6 +20,8 @@ internal static class Program /// The awaitable task. private static async Task Main(string[] args) { + await CheckForUpdate(); + var argResult = args.ExtractArguments(out Arguments arguments); Helper.InitLog(arguments.LogLevel, true); @@ -98,4 +99,33 @@ private static void PrintFooterHeader(bool header) if (header) Log.Information("Version: {version}", Assembly.GetExecutingAssembly().GetName().Version); } + + /// + /// Checks if an update is available. + /// + /// The awaitable task. + private static async Task CheckForUpdate() + { + try + { + await UpdateHelper.LoadReleaseInfoAsync(releaseInfo => + { + var updateMessage = $"Update available. New version: {releaseInfo.NewVersion}"; + const string link = $"Link: {UpdateHelper.GitHupUrl}"; + + var maxLength = updateMessage.Length > link.Length ? updateMessage.Length : link.Length; + + var line = "-".PadRight(maxLength + 2, '-'); // Add two for the spacer + Console.WriteLine($"+{line}+"); + Console.WriteLine($"| {updateMessage.PadRight(maxLength, ' ')} |"); + Console.WriteLine($"| {link.PadRight(maxLength, ' ')} |"); + Console.WriteLine($"+{line}+"); + Console.WriteLine(); + }); + } + catch + { + // Ignore + } + } } diff --git a/MsSql.ClassGenerator.Core/Business/ClassManager.cs b/MsSql.ClassGenerator.Core/Business/ClassManager.cs index 84db0d2..98910ef 100644 --- a/MsSql.ClassGenerator.Core/Business/ClassManager.cs +++ b/MsSql.ClassGenerator.Core/Business/ClassManager.cs @@ -1,4 +1,6 @@ -using MsSql.ClassGenerator.Core.Common; +using System.Reflection.Emit; +using System.Text; +using MsSql.ClassGenerator.Core.Common; using MsSql.ClassGenerator.Core.Model; using Serilog; @@ -12,7 +14,15 @@ public partial class ClassManager /// /// Occurs when progress was made. /// - public event EventHandler? ProgressEvent; + public event EventHandler? ProgressEvent; + + /// + /// Gets the EF Key code. + /// + /// + /// Note: The code is only generated when the option is set to . + /// + public EfKeyCodeResult EfKeyCode { get; private set; } = new(); /// /// Generates the classes out of the specified tables according to the specified options. @@ -23,6 +33,8 @@ public partial class ClassManager /// Will be thrown when the specified output directory doesn't exist. public async Task GenerateClassAsync(ClassGeneratorOptions options, List tables) { + EfKeyCode = new EfKeyCodeResult(); + // Step 0: Check the options. if (!Directory.Exists(options.Output)) throw new DirectoryNotFoundException($"The specified output ({options.Output}) folder doesn't exist."); @@ -40,6 +52,10 @@ public async Task GenerateClassAsync(ClassGeneratorOptions options, List @@ -248,4 +264,70 @@ private static string GetPropertyAttributes(ClassGeneratorOptions options, Colum return string.Join(Environment.NewLine, attributes.OrderBy(o => o.Key).Select(s => s.Value)); } + + /// + /// Generates the EF Key code. + /// + /// The list with the tables. + private void GenerateEfKeyCode(List tables) + { + // Get all tables which contains more than one key column + var tmpTables = tables.Where(w => w.Columns.Count(c => c.IsPrimaryKey) > 1).ToList(); + if (tmpTables.Count == 0) + return; + + var sb = PrepareStringBuilder(); + var count = 1; + foreach (var tableEntry in tmpTables) + { + // Add the entity + sb.AppendLine($"{Tab}modelBuilder.Entity<{tableEntry.ClassName}>().HasKey(k => new") + .AppendLine($"{Tab}{{"); + + // Get the key columns + var columnCount = 1; + var columns = tableEntry.Columns.Where(w => w.IsPrimaryKey).ToList(); + foreach (var columnEntry in columns) + { + var comma = columnCount++ != columns.Count ? "," : string.Empty; + + sb.AppendLine($"{Tab}{Tab}k.{columnEntry.PropertyName}{comma}"); + } + + // Add the closing brackets + sb.AppendLine($"{Tab}}});"); + + if (count++ != tmpTables.Count) + sb.AppendLine(); // Spacer + + } + + EfKeyCode = new EfKeyCodeResult + { + Code = FinalizeStringBuilder(), + TableCount = tmpTables.Count + }; + + return; + + StringBuilder PrepareStringBuilder() + { + var stringBuilder = new StringBuilder() + .AppendLine("/// ") + .AppendLine("protected override void OnModelCreating(ModelBuilder modelBuilder)") + .AppendLine("{"); + + return stringBuilder; + } + + // Adds the final code + string FinalizeStringBuilder() + { + sb.AppendLine("}"); + + return sb.ToString(); + } + } + + } \ No newline at end of file diff --git a/MsSql.ClassGenerator.Core/Business/UpdateHelper.cs b/MsSql.ClassGenerator.Core/Business/UpdateHelper.cs new file mode 100644 index 0000000..a0db1bc --- /dev/null +++ b/MsSql.ClassGenerator.Core/Business/UpdateHelper.cs @@ -0,0 +1,77 @@ +using MsSql.ClassGenerator.Core.Model; +using RestSharp; +using RestSharp.Serializers.NewtonsoftJson; +using Serilog; +using System.Reflection; + +namespace MsSql.ClassGenerator.Core.Business; + +/// +/// Provides the functions for the update. +/// +public static class UpdateHelper +{ + /// + /// Contains the URL of the latest release of the app. + /// + public const string GitHupUrl = "https://github.com/InvaderZim85/MsSql.ClassGenerator/releases/latest"; + + /// + /// Contains the URL of the latest release for the REST call. + /// + private const string GitHupApiUrl = + "https://api.github.com/repos/InvaderZim85/MsSql.ClassGenerator/releases/latest"; + + /// + /// Loads the release info of the latest release to determine if there is a new version. + /// + /// Will be executed when there is a new version. + /// The awaitable task. + public static async Task LoadReleaseInfoAsync(Action callback) + { + try + { + var client = new RestClient(GitHupApiUrl, + configureSerialization: s => s.UseNewtonsoftJson()); + + client.AddDefaultHeader("accept", "application/vnd.github.v3+json"); + + var request = new RestRequest(); + var response = await client.GetAsync(request); + + // This method also checks if the response is null + if (IsNewVersionAvailable(response)) + callback(response!); + } + catch (Exception ex) + { + Log.Warning(ex, "Error while loading the latest release info."); + } + } + + /// + /// Checks if an update is available + /// + /// The infos of the latest release + /// when there is a new version, otherwise + private static bool IsNewVersionAvailable(ReleaseInfo? releaseInfo) + { + if (releaseInfo == null) + return false; + + if (!Version.TryParse(releaseInfo.TagName.Replace("v", ""), out var releaseVersion)) + { + Log.Warning("Can't determine version of the latest release. Tag value: {value}", releaseInfo.TagName); + return false; + } + + var currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (currentVersion == null) + return false; + + releaseInfo.CurrentVersion = currentVersion; + releaseInfo.NewVersion = releaseVersion; + + return releaseInfo.UpdateAvailable; + } +} \ No newline at end of file diff --git a/MsSql.ClassGenerator.Core/Common/Extensions.cs b/MsSql.ClassGenerator.Core/Common/Extensions.cs index 1e9be63..dadb166 100644 --- a/MsSql.ClassGenerator.Core/Common/Extensions.cs +++ b/MsSql.ClassGenerator.Core/Common/Extensions.cs @@ -118,4 +118,24 @@ public static bool StartsWithNumber(this string value) { return !string.IsNullOrWhiteSpace(value) && int.TryParse(value[0].ToString(), out _); } + + /// + /// Converts the value into a readable size + /// + /// The value + /// The divider (optional) + /// The converted size + public static string ConvertSize(this long value, int divider = 1024) + { + return value switch + { + _ when value < divider => $"{value:N0} Bytes", + _ when value >= divider && value < Math.Pow(divider, 2) => $"{value / divider:N2} KB", + _ when value >= Math.Pow(divider, 2) && value < Math.Pow(divider, 3) => + $"{value / Math.Pow(divider, 2):N2} MB", + _ when value >= Math.Pow(divider, 3) && value <= Math.Pow(divider, 4) => $"{value / Math.Pow(divider, 3):N2} GB", + _ when value >= Math.Pow(divider, 4) => $"{value / Math.Pow(divider, 4)} TB", + _ => value.ToString("N0") + }; + } } diff --git a/MsSql.ClassGenerator.Core/Common/Helper.cs b/MsSql.ClassGenerator.Core/Common/Helper.cs index 9d3456f..2f7f4b8 100644 --- a/MsSql.ClassGenerator.Core/Common/Helper.cs +++ b/MsSql.ClassGenerator.Core/Common/Helper.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Serilog; using Serilog.Events; +using System.Diagnostics; namespace MsSql.ClassGenerator.Core.Common; @@ -90,4 +91,21 @@ public static List GetModifierList() "protected internal" ]; } + + /// + /// Opens the specified link + /// + /// The url of the link + public static void OpenLink(string url) + { + try + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); + } + catch + { + // Ignore + } + } } diff --git a/MsSql.ClassGenerator.Core/Data/BaseRepo.cs b/MsSql.ClassGenerator.Core/Data/BaseRepo.cs index db527b5..1cd1572 100644 --- a/MsSql.ClassGenerator.Core/Data/BaseRepo.cs +++ b/MsSql.ClassGenerator.Core/Data/BaseRepo.cs @@ -30,7 +30,7 @@ protected SqlConnection GetConnection() DataSource = server, IntegratedSecurity = true, TrustServerCertificate = true, - ApplicationName = nameof(MsSql.ClassGenerator) + ApplicationName = nameof(ClassGenerator) }; if (!string.IsNullOrWhiteSpace(database)) diff --git a/MsSql.ClassGenerator.Core/Model/Asset.cs b/MsSql.ClassGenerator.Core/Model/Asset.cs new file mode 100644 index 0000000..8ecd9d3 --- /dev/null +++ b/MsSql.ClassGenerator.Core/Model/Asset.cs @@ -0,0 +1,32 @@ +using MsSql.ClassGenerator.Core.Common; +using Newtonsoft.Json; + +namespace MsSql.ClassGenerator.Core.Model; + +/// +/// Represents the assets of the last release +/// +public sealed class Asset +{ + /// + /// Gets or sets the name of the zip file + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the size of the latest release (in bytes) + /// + public long Size { get; set; } + + /// + /// Gets the size of the latest release in a readable format + /// + [JsonIgnore] + public string SizeView => Size.ConvertSize(); + + /// + /// Gets or sets the url of the release + /// + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MsSql.ClassGenerator.Core/Model/EfKeyCodeResult.cs b/MsSql.ClassGenerator.Core/Model/EfKeyCodeResult.cs new file mode 100644 index 0000000..cf795bc --- /dev/null +++ b/MsSql.ClassGenerator.Core/Model/EfKeyCodeResult.cs @@ -0,0 +1,22 @@ +namespace MsSql.ClassGenerator.Core.Model; + +/// +/// Provides the key code result. +/// +public sealed class EfKeyCodeResult +{ + /// + /// Gets the code. + /// + public string Code { get; init; } = string.Empty; + + /// + /// Gets the amount of tables which contains multiple keys. + /// + public int TableCount { get; init; } + + /// + /// Gets the value which indicates whether the code is empty. + /// + public bool IsEmpty => string.IsNullOrWhiteSpace(Code); +} \ No newline at end of file diff --git a/MsSql.ClassGenerator.Core/Model/ReleaseInfo.cs b/MsSql.ClassGenerator.Core/Model/ReleaseInfo.cs new file mode 100644 index 0000000..a57570f --- /dev/null +++ b/MsSql.ClassGenerator.Core/Model/ReleaseInfo.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; + +namespace MsSql.ClassGenerator.Core.Model; + +/// +/// Provides the information of the latest release +/// +public sealed class ReleaseInfo +{ + /// + /// Gets or sets the html url of the release + /// + [JsonProperty("html_url")] + public string HtmlUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the tag + /// + [JsonProperty("tag_name")] + public string TagName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the release + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the publish date of the release + /// + [JsonProperty("published_at")] + public DateTime PublishedAt { get; set; } + + /// + /// Gets the published at date as formatted string + /// + [JsonIgnore] + public string PublishedAtView => PublishedAt.ToString("yyyy-MM-dd HH:mm:ss"); + + /// + /// Gets or sets the body (message) of the release + /// + public string Body { get; set; } = string.Empty; + + /// + /// Gets or sets the with the assets + /// + public List Assets { get; set; } = []; + + /// + /// Gets or sets the number current version + /// + public Version CurrentVersion { get; set; } = new(); + + /// + /// Gets or sets the number of the new version + /// + public Version NewVersion { get; set; } = new(); + + /// + /// Gets the value which indicates whether a new version is available. + /// + public bool UpdateAvailable => NewVersion > CurrentVersion; +} diff --git a/MsSql.ClassGenerator.Core/MsSql.ClassGenerator.Core.csproj b/MsSql.ClassGenerator.Core/MsSql.ClassGenerator.Core.csproj index c0cdd14..b883681 100644 --- a/MsSql.ClassGenerator.Core/MsSql.ClassGenerator.Core.csproj +++ b/MsSql.ClassGenerator.Core/MsSql.ClassGenerator.Core.csproj @@ -1,11 +1,11 @@ - + net9.0-windows enable enable - 25.43.0.756 - 25.43.0.756 - 25.43.0.756 + 24.43.0.756 + 24.43.0.756 + 24.43.0.756 @@ -14,6 +14,8 @@ + + diff --git a/MsSql.ClassGenerator/Common/Enums/EntryType.cs b/MsSql.ClassGenerator/Common/Enums/EntryType.cs new file mode 100644 index 0000000..d482dd6 --- /dev/null +++ b/MsSql.ClassGenerator/Common/Enums/EntryType.cs @@ -0,0 +1,17 @@ +namespace MsSql.ClassGenerator.Common.Enums; + +/// +/// Provides the different entry types. +/// +public enum EntryType +{ + /// + /// Represents a table. + /// + Table, + + /// + /// Represents a column. + /// + Column +} \ No newline at end of file diff --git a/MsSql.ClassGenerator/Model/TableColumnDto.cs b/MsSql.ClassGenerator/Model/TableColumnDto.cs index bdcfe0f..cc04601 100644 --- a/MsSql.ClassGenerator/Model/TableColumnDto.cs +++ b/MsSql.ClassGenerator/Model/TableColumnDto.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; +using MsSql.ClassGenerator.Common.Enums; using MsSql.ClassGenerator.Core.Model; namespace MsSql.ClassGenerator.Model; @@ -56,11 +57,21 @@ partial void OnAliasChanged(string value) [ObservableProperty] private bool _use = true; + /// + /// Gets or sets the value which indicates whether the column is part of the key. + /// + public bool KeyColumn { get; set; } + /// /// Gets or sets the original item (table / column) /// public object? OriginalItem { get; set; } + /// + /// Gets or sets the type of the entry. + /// + public EntryType Type { get; } + /// /// Gets or sets the list with the columns. /// @@ -82,18 +93,21 @@ public TableColumnDto(TableEntry table) Columns = table.Columns.Select(s => new TableColumnDto(s)).ToList(); Use = true; OriginalItem = table; + Type = EntryType.Table; } /// /// Creates a new column instance. /// /// The source column. - public TableColumnDto(ColumnEntry column) + private TableColumnDto(ColumnEntry column) { Name = column.Name; Alias = column.Alias; Position = column.Order; Use = true; OriginalItem = column; + Type = EntryType.Column; + KeyColumn = column.IsPrimaryKey; } } \ No newline at end of file diff --git a/MsSql.ClassGenerator/MsSql.ClassGenerator.csproj b/MsSql.ClassGenerator/MsSql.ClassGenerator.csproj index ea56699..bf39e00 100644 --- a/MsSql.ClassGenerator/MsSql.ClassGenerator.csproj +++ b/MsSql.ClassGenerator/MsSql.ClassGenerator.csproj @@ -1,13 +1,13 @@ - + WinExe net9.0-windows enable enable true - 25.43.0.756 - 25.43.0.756 - 25.43.0.756 + 24.43.0.756 + 24.43.0.756 + 24.43.0.756 x64 icon.ico @@ -15,10 +15,12 @@ + + diff --git a/MsSql.ClassGenerator/Ui/UiHelper.cs b/MsSql.ClassGenerator/Ui/UiHelper.cs new file mode 100644 index 0000000..fbaf0a0 --- /dev/null +++ b/MsSql.ClassGenerator/Ui/UiHelper.cs @@ -0,0 +1,22 @@ +using ICSharpCode.AvalonEdit; +using System.Windows.Media; + +namespace MsSql.ClassGenerator.Ui; + +/// +/// Provides several helper methods for the UI. +/// +internal static class UiHelper +{ + + /// + /// Init the avalon editor. + /// + /// The editor. + public static void InitAvalonEditor(this TextEditor editor) + { + editor.Options.HighlightCurrentLine = true; + editor.Options.ConvertTabsToSpaces = true; // We hate tabs... + editor.Foreground = new SolidColorBrush(Colors.White); + } +} \ No newline at end of file diff --git a/MsSql.ClassGenerator/Ui/View/CodeWindow.xaml b/MsSql.ClassGenerator/Ui/View/CodeWindow.xaml new file mode 100644 index 0000000..6bfbfe0 --- /dev/null +++ b/MsSql.ClassGenerator/Ui/View/CodeWindow.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + /// The provided arguments. - public async void InitViewModel(Arguments arguments) + /// The awaitable task. + public async Task InitViewModelAsync(Arguments arguments) { try { + // Set the version + VersionInfo = $"v{Assembly.GetExecutingAssembly().GetName().Version}"; + + // Start the update check + CheckUpdate(); + + // Load the data ModifierList = Helper.GetModifierList().ToObservableCollection(); SelectedModifier = ModifierList.FirstOrDefault() ?? "public"; @@ -385,7 +431,7 @@ private async Task ConnectAsync() if (SelectedServer.AutoConnect) await SelectAsync(); - SetAppTitle(); + SetAppInfo(); } catch (Exception ex) { @@ -428,7 +474,7 @@ private async Task SelectAsync() FilterTables(); - SetAppTitle(); + SetAppInfo(); IsConnected = true; } @@ -602,12 +648,15 @@ private void FilterColumns() /// The observable collection private static ObservableCollection FilterValues(List source, string filter) { - return string.IsNullOrWhiteSpace(filter) - ? source.ToObservableCollection() - : source.Where(w => w.Name.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || + var tmpList = string.IsNullOrWhiteSpace(filter) + ? source + : [.. source.Where(w => w.Name.Contains(filter, StringComparison.InvariantCultureIgnoreCase) || (!string.IsNullOrWhiteSpace(w.Schema) && w.Schema.Contains(filter, - StringComparison.InvariantCultureIgnoreCase))) - .ToObservableCollection(); + StringComparison.InvariantCultureIgnoreCase)))]; + + return tmpList.FirstOrDefault()?.Type == EntryType.Table + ? tmpList.OrderBy(o => o.Name).ToObservableCollection() + : tmpList.OrderBy(o => o.Position).ToObservableCollection(); } #endregion @@ -662,6 +711,8 @@ private static void SetSelection(IEnumerable source, SelectionTy [RelayCommand] private async Task GenerateClassesAsync() { + ButtonShowEfKeyCodeEnabled = false; + if (!ValidateInput(out var errorMessage)) { await ShowMessageAsync("Generation", errorMessage); @@ -683,6 +734,9 @@ private async Task GenerateClassesAsync() // Save the current options await SaveOptionsAsync(options); + + ButtonShowEfKeyCodeEnabled = !_efKeyCode.IsEmpty; + _efKeyCode = classManager.EfKeyCode; } catch (Exception ex) { @@ -694,6 +748,26 @@ private async Task GenerateClassesAsync() } } + /// + /// Occurs when the user hits the "Show EF Key Code" button. + /// + /// + /// Opens the code window with the generated code. + /// + [RelayCommand] + private void ShowEfKeyCode() + { + if (_efKeyCode.IsEmpty) + return; + + var codeWindow = new CodeWindow(_efKeyCode) + { + Owner = GetMainWindow() + }; + + codeWindow.ShowDialog(); + } + /// /// Validates the input. /// @@ -767,6 +841,21 @@ private static async Task SaveOptionsAsync(ClassGeneratorOptions options) } #endregion + #region Various + + /// + /// Occurs when the user hits the update button (title bar). + /// + /// + /// Opens the GitHub page with the latest version. + /// + [RelayCommand] + private static void OpenGitHubPage() + { + Helper.OpenLink(UpdateHelper.GitHupUrl); + } + #endregion + #endregion #region Various @@ -774,17 +863,18 @@ private static async Task SaveOptionsAsync(ClassGeneratorOptions options) /// /// Sets the app title. /// - private void SetAppTitle() + private void SetAppInfo() { - var tmpName = AppTitleDefault; + var info = string.Empty; if (SelectedServer != null) - tmpName += $" - Server: '{SelectedServer.Name}'"; + info += $"Server: '{SelectedServer.Name}'"; if (!string.IsNullOrEmpty(SelectedDatabase)) - tmpName += $" | Database: '{SelectedDatabase}'"; + info += $" | Database: '{SelectedDatabase}'"; - AppTitle = tmpName; + AppTitle = $"{AppTitleDefault} - {info}"; + Info = $"Connected - {info}"; } /// @@ -797,5 +887,27 @@ private void Reset() Columns.Clear(); Tables.Clear(); } + + /// + /// Checks if a new version is available + /// + private void CheckUpdate() + { + // The "Forget()" method is used to let the async task run without waiting. + // More information: https://docs.microsoft.com/en-us/answers/questions/186037/taskrun-without-wait.html + // To use "Forget" you need the following nuget package: https://www.nuget.org/packages/Microsoft.VisualStudio.Threading/ + UpdateHelper.LoadReleaseInfoAsync(SetReleaseInfo).Forget(); + } + + /// + /// Sets the release info and shows the update button + /// + /// The infos of the latest release + private void SetReleaseInfo(ReleaseInfo releaseInfo) + { + ButtonUpdateVisibility = !string.IsNullOrWhiteSpace(releaseInfo.Name) ? Visibility.Visible : Visibility.Hidden; + UpdateInfo = $"Update available! New version: v{releaseInfo.NewVersion}"; + } + #endregion }