From 0df6e4843818fa36fa60609744793c0712785c0c Mon Sep 17 00:00:00 2001 From: electricvampire Date: Sat, 5 Oct 2024 17:10:06 +0530 Subject: [PATCH 1/3] Show query even if queryplan times out --- .../QueryPlanUserControl.xaml.cs | 32 ++++ .../EFCoreQueryableObjectSource.cs | 154 ++++++++++++++---- 2 files changed, 152 insertions(+), 34 deletions(-) diff --git a/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs b/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs index 4c1fb5c..ecd2db9 100644 --- a/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs +++ b/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs @@ -42,6 +42,7 @@ private void QueryPlanUserControlUnloaded(object sender, RoutedEventArgs e) protected override async void OnInitialized(EventArgs e) #pragma warning restore VSTHRD100 // Avoid async void methods { + var query = string.Empty; try { base.OnInitialized(e); @@ -50,6 +51,8 @@ protected override async void OnInitialized(EventArgs e) var environment = await CoreWebView2Environment.CreateAsync(userDataFolder: Path.Combine(AssemblyLocation, "WVData")); await webView.EnsureCoreWebView2Async(environment); + query = await GetQueryAsync(); + #if !DEBUG webView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false; webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; @@ -84,10 +87,39 @@ protected override async void OnInitialized(EventArgs e) } catch (Exception ex) { + if (!string.IsNullOrEmpty(query)) + webView.CoreWebView2.NavigateToString(query); MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } + private async Task GetQueryAsync() + { + var query = string.Empty; + var response = await visualizerTarget.ObjectSource.RequestDataAsync(ConvertStringToReadOnlySequence("GetQuery"), CancellationToken.None); + if (response.HasValue) + { + using var stream = response.Value.AsStream(); + using var binaryReader = new BinaryReader(stream, Encoding.Default); + var isError = binaryReader.ReadBoolean(); + if (!isError) + { + query = binaryReader.ReadString(); + } + } + return query; + } + private static ReadOnlySequence ConvertStringToReadOnlySequence(string input) + { + byte[] byteArray = Encoding.UTF8.GetBytes(input); + + ReadOnlyMemory readOnlyMemory = new ReadOnlyMemory(byteArray); + + ReadOnlySequence readOnlySequence = new ReadOnlySequence(readOnlyMemory); + + return readOnlySequence; + } + private void ButtonReviewClick(object sender, RoutedEventArgs e) { StartProcess("https://marketplace.visualstudio.com/items?itemName=GiorgiDalakishvili.EFCoreVisualizer&ssr=false#review-details"); diff --git a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs index 7763ba0..d9fff49 100644 --- a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs +++ b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs @@ -6,6 +6,9 @@ using System.Linq; using System.Text; using System.Text.Encodings.Web; +using System.Threading.Tasks; +using System.Net; +using Microsoft.EntityFrameworkCore.Storage; namespace IQueryableObjectSource { @@ -22,55 +25,138 @@ public override void TransferData(object target, Stream incomingData, Stream out try { - using var command = queryable.CreateDbCommand(); - var provider = GetDatabaseProvider(command); - - if (provider == null) + var dbOperation = ConvertStreamToString(incomingData); + switch (dbOperation) { - return; + case "GetQuery": + HandleGetQuery(queryable, outgoingData); + break; + + default: + HandleGetQueryPlan(queryable, incomingData, outgoingData); + break; } + } + catch (Exception ex) + { + WriteError(outgoingData, ex.Message); + } + } + private void HandleGetQuery(IQueryable queryable, Stream outgoingData) + { + using var queryWriter = new BinaryWriter(outgoingData, Encoding.Default, true); + queryWriter.Write(false); // Indicates no error + queryWriter.Write(GenerateHtml(queryable.ToQueryString())); + } + private void HandleGetQueryPlan(IQueryable queryable, Stream incomingData, Stream outgoingData) + { + using var command = queryable.CreateDbCommand(); + var provider = GetDatabaseProvider(command); - var query = queryable.ToQueryString(); - var rawPlan = provider.ExtractPlan(); + if (provider == null) + { + return; + } - var buffer = new byte[3]; - var isBackgroundDarkColor = false; + var query = queryable.ToQueryString(); + var rawPlan = provider.ExtractPlan(); - var r = 255; - var g = 255; - var b = 255; + var (r, g, b) = ReadBackgroundColor(incomingData); + var isBackgroundDarkColor = r * 0.2126 + g * 0.7152 + b * 0.0722 < 255 / 2.0; - if (incomingData.Read(buffer, 0, buffer.Length) == buffer.Length) - { - r = buffer[0]; - g = buffer[1]; - b = buffer[2]; - } + var planFile = GeneratePlanFile(provider, query, rawPlan, r, g, b, isBackgroundDarkColor); + + using var writer = new BinaryWriter(outgoingData, Encoding.Default, true); + writer.Write(false); // Indicates no error + writer.Write(planFile); + } + private (int r, int g, int b) ReadBackgroundColor(Stream incomingData) + { + var buffer = new byte[3]; + var r = 255; + var g = 255; + var b = 255; + + if (incomingData.Read(buffer, 0, buffer.Length) == buffer.Length) + { + r = buffer[0]; + g = buffer[1]; + b = buffer[2]; + } - isBackgroundDarkColor = r * 0.2126 + g * 0.7152 + b * 0.0722 < 255 / 2.0; + return (r, g, b); + } - var planFile = Path.Combine(provider.GetPlanDirectory(ResourcesLocation), Path.ChangeExtension(Path.GetRandomFileName(), "html")); + private string GeneratePlanFile(DatabaseProvider provider, string query, string rawPlan, int r, int g, int b, bool isBackgroundDarkColor) + { + var planDirectory = provider.GetPlanDirectory(ResourcesLocation); + var planFile = Path.Combine(planDirectory, Path.ChangeExtension(Path.GetRandomFileName(), "html")); - var planPageHtml = File.ReadAllText(Path.Combine(provider.GetPlanDirectory(ResourcesLocation), "template.html")) - .Replace("{backColor}", $"rgb({r} {g} {b})") - .Replace("{textColor}", isBackgroundDarkColor ? "white" : "black") - .Replace("{plan}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(rawPlan).Replace("'", "\\'")) - .Replace("{query}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(query).Replace("'", "\\'")); + var planPageHtml = File.ReadAllText(Path.Combine(planDirectory, "template.html")) + .Replace("{backColor}", $"rgb({r} {g} {b})") + .Replace("{textColor}", isBackgroundDarkColor ? "white" : "black") + .Replace("{plan}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(rawPlan).Replace("'", "\\'")) + .Replace("{query}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(query).Replace("'", "\\'")); - File.WriteAllText(planFile, planPageHtml); + File.WriteAllText(planFile, planPageHtml); - using var writer = new BinaryWriter(outgoingData, Encoding.Default, true); - writer.Write(false); - writer.Write(planFile); - } - catch (Exception ex) + return planFile; + } + + private void WriteError(Stream outgoingData, string errorMessage) + { + using var writer = new BinaryWriter(outgoingData, Encoding.Default, true); + writer.Write(true); // Indicates an error occurred + writer.Write(errorMessage); + } + + public static string ConvertStreamToString(Stream stream) + { + using (MemoryStream memoryStream = new MemoryStream()) { - using var writer = new BinaryWriter(outgoingData, Encoding.Default, true); - writer.Write(true); - writer.Write(ex.Message); + stream.CopyTo(memoryStream); + byte[] byteArray = memoryStream.ToArray(); + + //Try to convert byte array to a string using UTF-8 encoding + try + { + string result = Encoding.UTF8.GetString(byteArray); + return result; + } + catch (Exception) + { + return "GetQueryPlan"; + } } } + private static string GenerateHtml(string query) + { + string escapedQuery = WebUtility.HtmlEncode(query); + + // Simple HTML structure to display the query + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.AppendLine(""); + htmlBuilder.AppendLine("Query Plan Visualizer"); + htmlBuilder.AppendLine(""); + htmlBuilder.AppendLine(""); + htmlBuilder.AppendLine(""); + htmlBuilder.AppendLine("

SQL Query

"); + htmlBuilder.AppendLine("
"); + htmlBuilder.AppendLine("
" + escapedQuery + "
"); + htmlBuilder.AppendLine("
"); + htmlBuilder.AppendLine(""); + htmlBuilder.AppendLine(""); + + return htmlBuilder.ToString(); + } + + private static DatabaseProvider GetDatabaseProvider(DbCommand command) { return command.GetType().FullName switch From 95f03cfb75e8a27f07837238148f6b2e3b9309ec Mon Sep 17 00:00:00 2001 From: electricvampire Date: Sat, 5 Oct 2024 17:54:57 +0530 Subject: [PATCH 2/3] Instead of passing string for operation, changed it to byte enum --- .../EFCore.Visualizer.csproj | 4 +++ .../QueryPlanUserControl.xaml.cs | 14 +++----- .../EFCoreQueryableObjectSource.cs | 33 +++++++++---------- src/IQueryableObjectSource/OperationType.cs | 14 ++++++++ 4 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 src/IQueryableObjectSource/OperationType.cs diff --git a/src/EFCore.Visualizer/EFCore.Visualizer.csproj b/src/EFCore.Visualizer/EFCore.Visualizer.csproj index 6eb2dbf..39f8bfe 100644 --- a/src/EFCore.Visualizer/EFCore.Visualizer.csproj +++ b/src/EFCore.Visualizer/EFCore.Visualizer.csproj @@ -60,6 +60,10 @@ + + + + diff --git a/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs b/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs index ecd2db9..008e402 100644 --- a/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs +++ b/src/EFCore.Visualizer/QueryPlanUserControl.xaml.cs @@ -7,6 +7,7 @@ using System.Text; using System.Windows; using System.Windows.Controls; +using IQueryableObjectSource; namespace EFCore.Visualizer { @@ -96,7 +97,7 @@ protected override async void OnInitialized(EventArgs e) private async Task GetQueryAsync() { var query = string.Empty; - var response = await visualizerTarget.ObjectSource.RequestDataAsync(ConvertStringToReadOnlySequence("GetQuery"), CancellationToken.None); + var response = await visualizerTarget.ObjectSource.RequestDataAsync(ConvertEnumToReadOnlySequence(OperationType.GetQuery),CancellationToken.None); if (response.HasValue) { using var stream = response.Value.AsStream(); @@ -109,15 +110,10 @@ private async Task GetQueryAsync() } return query; } - private static ReadOnlySequence ConvertStringToReadOnlySequence(string input) + private ReadOnlySequence ConvertEnumToReadOnlySequence(OperationType operation) { - byte[] byteArray = Encoding.UTF8.GetBytes(input); - - ReadOnlyMemory readOnlyMemory = new ReadOnlyMemory(byteArray); - - ReadOnlySequence readOnlySequence = new ReadOnlySequence(readOnlyMemory); - - return readOnlySequence; + var operationByte = new byte[] { (byte)operation }; + return new ReadOnlySequence(operationByte); } private void ButtonReviewClick(object sender, RoutedEventArgs e) diff --git a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs index d9fff49..358e573 100644 --- a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs +++ b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs @@ -25,13 +25,16 @@ public override void TransferData(object target, Stream incomingData, Stream out try { - var dbOperation = ConvertStreamToString(incomingData); - switch (dbOperation) + var operationType = GetOperationType(incomingData); + switch (operationType) { - case "GetQuery": + case OperationType.GetQuery: HandleGetQuery(queryable, outgoingData); break; + case OperationType.NotSupported: + throw new InvalidOperationException("Unknown operation type."); + default: HandleGetQueryPlan(queryable, incomingData, outgoingData); break; @@ -110,23 +113,17 @@ private void WriteError(Stream outgoingData, string errorMessage) writer.Write(errorMessage); } - public static string ConvertStreamToString(Stream stream) + public static OperationType GetOperationType(Stream stream) { - using (MemoryStream memoryStream = new MemoryStream()) + try { - stream.CopyTo(memoryStream); - byte[] byteArray = memoryStream.ToArray(); - - //Try to convert byte array to a string using UTF-8 encoding - try - { - string result = Encoding.UTF8.GetString(byteArray); - return result; - } - catch (Exception) - { - return "GetQueryPlan"; - } + var operationBuffer = new byte[1]; + stream.Read(operationBuffer, 0, 1); + return (OperationType)operationBuffer[0]; + } + catch (Exception) + { + return OperationType.NotSupported; } } diff --git a/src/IQueryableObjectSource/OperationType.cs b/src/IQueryableObjectSource/OperationType.cs new file mode 100644 index 0000000..263580a --- /dev/null +++ b/src/IQueryableObjectSource/OperationType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IQueryableObjectSource +{ + public enum OperationType : byte + { + GetQuery = 1, + NotSupported = 9 + } +} From f6dc9aca82f560193c95c03a310683273851b83c Mon Sep 17 00:00:00 2001 From: electricvampire Date: Sat, 5 Oct 2024 18:11:14 +0530 Subject: [PATCH 3/3] Templatized query html page --- .../EFCore.Visualizer.csproj | 7 ++++ .../Resources/Common/template.html | 36 +++++++++++++++++++ .../EFCoreQueryableObjectSource.cs | 29 +++++---------- 3 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 src/EFCore.Visualizer/Resources/Common/template.html diff --git a/src/EFCore.Visualizer/EFCore.Visualizer.csproj b/src/EFCore.Visualizer/EFCore.Visualizer.csproj index 39f8bfe..0ea4f3c 100644 --- a/src/EFCore.Visualizer/EFCore.Visualizer.csproj +++ b/src/EFCore.Visualizer/EFCore.Visualizer.csproj @@ -10,12 +10,16 @@ + + + true + @@ -58,6 +62,9 @@ true + + true + diff --git a/src/EFCore.Visualizer/Resources/Common/template.html b/src/EFCore.Visualizer/Resources/Common/template.html new file mode 100644 index 0000000..7cc6610 --- /dev/null +++ b/src/EFCore.Visualizer/Resources/Common/template.html @@ -0,0 +1,36 @@ + + + + Query Plan Visualizer + + + +

SQL Query

+
+
{query}
+
+ + diff --git a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs index 358e573..f41a57e 100644 --- a/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs +++ b/src/IQueryableObjectSource/EFCoreQueryableObjectSource.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Net; using Microsoft.EntityFrameworkCore.Storage; +using System.Diagnostics; namespace IQueryableObjectSource { @@ -130,27 +131,15 @@ public static OperationType GetOperationType(Stream stream) private static string GenerateHtml(string query) { string escapedQuery = WebUtility.HtmlEncode(query); + string templatePath = Path.Combine(ResourcesLocation,"Common", "template.html"); + if (!File.Exists(templatePath)) + { + throw new FileNotFoundException("Common Query template file not found", templatePath); + } - // Simple HTML structure to display the query - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("Query Plan Visualizer"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine("

SQL Query

"); - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine("
" + escapedQuery + "
"); - htmlBuilder.AppendLine("
"); - htmlBuilder.AppendLine(""); - htmlBuilder.AppendLine(""); - - return htmlBuilder.ToString(); + string templateContent = File.ReadAllText(templatePath); + string finalHtml = templateContent.Replace("{query}", escapedQuery); + return finalHtml; }