From 3fe80f8a55c81ac3ce0ea3b14b8a0284e3ee56ba Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:21:55 -0500 Subject: [PATCH 1/7] Adding support for Microsoft.Data.SqlClient --- CommandAsSql/CommandAsSql.csproj | 7 +- CommandAsSql/ExtensionMethods.cs | 14 +- CommandAsSql/ExtensionMethodsMicrosoft.cs | 256 ++++++++++++++++++ XUnitTestCommandAsSql/UnitTest1.cs | 10 +- .../XUnitTestCommandAsSql.csproj | 12 +- 5 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 CommandAsSql/ExtensionMethodsMicrosoft.cs diff --git a/CommandAsSql/CommandAsSql.csproj b/CommandAsSql/CommandAsSql.csproj index 303c64a..8637e63 100644 --- a/CommandAsSql/CommandAsSql.csproj +++ b/CommandAsSql/CommandAsSql.csproj @@ -26,11 +26,12 @@ - + + - - + + diff --git a/CommandAsSql/ExtensionMethods.cs b/CommandAsSql/ExtensionMethods.cs index 84c2ddf..4495e8f 100644 --- a/CommandAsSql/ExtensionMethods.cs +++ b/CommandAsSql/ExtensionMethods.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Data; using System.Data.SqlClient; +using System.Globalization; +using System.Text; using System.Text.RegularExpressions; -namespace CommandAsSql +namespace CommandAsSql.System { /// /// Extension method to parse a SqlCommand as string with filled SqlParameters. Makes it easy to paste the string in a Database Management tool to debug and profile etc. @@ -101,7 +103,7 @@ public static string ParameterValueForSQL(this SqlParameter param) return (paramValue.ToBooleanOrDefault(false)) ? "1" : "0"; case SqlDbType.Structured: - var sb = new System.Text.StringBuilder(); + var sb = new StringBuilder(); var dt = (DataTable)paramValue; sb.Append("declare ").Append(param.ParameterName).Append(" ").AppendLine(param.TypeName); @@ -141,7 +143,7 @@ public static string ParameterValueForSQL(this SqlParameter param) case SqlDbType.Decimal: case SqlDbType.Float: - return ((double)paramValue).ToString(System.Globalization.CultureInfo.InvariantCulture).Replace("'", "''"); + return ((double)paramValue).ToString(CultureInfo.InvariantCulture).Replace("'", "''"); default: return paramValue.ToString().Replace("'", "''"); @@ -169,7 +171,7 @@ private static List GetStructured(this SqlParameterCollection para /// public static string CommandAsSql(this SqlCommand command) { - var sql = new System.Text.StringBuilder(); + var sql = new StringBuilder(); if (command.Connection != null) sql.Append("use ").Append(command.Connection.Database).AppendLine(";"); @@ -191,7 +193,7 @@ public static string CommandAsSql(this SqlCommand command) return sql.ToString(); } - private static void CommandAsSql_Text(this SqlCommand command, System.Text.StringBuilder sql) + private static void CommandAsSql_Text(this SqlCommand command, StringBuilder sql) { string query = command.CommandText; @@ -201,7 +203,7 @@ private static void CommandAsSql_Text(this SqlCommand command, System.Text.Strin sql.AppendLine(query); } - private static void CommandAsSql_StoredProcedure(this SqlCommand command, System.Text.StringBuilder sql) + private static void CommandAsSql_StoredProcedure(this SqlCommand command, StringBuilder sql) { sql.AppendLine("declare @return_value int;"); diff --git a/CommandAsSql/ExtensionMethodsMicrosoft.cs b/CommandAsSql/ExtensionMethodsMicrosoft.cs new file mode 100644 index 0000000..c7c4443 --- /dev/null +++ b/CommandAsSql/ExtensionMethodsMicrosoft.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Microsoft.Data.SqlClient; +using System.Text.RegularExpressions; +using System.Text; +using System.Globalization; + +namespace CommandAsSql.Microsoft +{ + /// + /// Extension method to parse a SqlCommand as string with filled SqlParameters. Makes it easy to paste the string in a Database Management tool to debug and profile etc. + /// + public static class ExtensionMethods + { + #region Boolean Helpers + /// + /// extension method overload for a string + /// + /// + /// + /// + public static bool ToBooleanOrDefault(this string s, bool defaultValue) + { + return ToBooleanOrDefault((object)s, defaultValue); + } + + /// + /// extension method to get a bool value from an object + /// + /// + /// + /// + public static bool ToBooleanOrDefault(this object o, bool defaultValue) + { + bool result = defaultValue; + + if (o != null) + { + try + { + switch (o.ToString().ToLower()) + { + case "yes": + case "true": + case "ok": + case "y": + result = true; + break; + + case "no": + case "false": + case "n": + result = false; + break; + + default: + result = bool.Parse(o.ToString()); + break; + } + } + catch + { + } + } + + return result; + } + + #endregion + + #region SQL Helpers + + /// + /// Turns a parameter object to string + /// + /// SqlParameter + /// + public static string ParameterValueForSQL(this SqlParameter param) + { + object paramValue = param.Value; //assuming param isn't null + + if (paramValue == null) //TODO: should probably use DBNull.Value instead or in combination with this + return "NULL"; //TODO: naive code, won't work as is, need to replace later on = NULL with IS NULL at non-Update queries + + switch (param.SqlDbType) + { + case SqlDbType.Char: + case SqlDbType.NChar: + case SqlDbType.NText: + case SqlDbType.NVarChar: + case SqlDbType.Text: + case SqlDbType.Time: + case SqlDbType.VarChar: + case SqlDbType.Xml: + case SqlDbType.Date: + case SqlDbType.DateTime: + case SqlDbType.DateTime2: + case SqlDbType.DateTimeOffset: + return $"'{paramValue.ToString().Replace("'", "''")}'"; + + case SqlDbType.Bit: + return (paramValue.ToBooleanOrDefault(false)) ? "1" : "0"; + + case SqlDbType.Structured: + var sb = new StringBuilder(); + var dt = (DataTable)paramValue; + + sb.Append("declare ").Append(param.ParameterName).Append(" ").AppendLine(param.TypeName); + + foreach (DataRow dr in dt.Rows) + { + sb.Append("insert ").Append(param.ParameterName).Append(" values ("); + + for (int colIndex = 0; colIndex < dt.Columns.Count; colIndex++) + { + switch (Type.GetTypeCode(dr[colIndex].GetType())) + { + case TypeCode.Boolean: + sb.Append(Convert.ToInt32(dr[colIndex])); + break; + + case TypeCode.String: + sb.Append("'").Append(dr[colIndex]).Append("'"); + break; + + case TypeCode.DateTime: + sb.Append("'").Append(Convert.ToDateTime(dr[colIndex]).ToString("yyyy-MM-dd HH:mm")).Append("'"); + break; + + default: + sb.Append(dr[colIndex]); break; + } + + sb.Append(", "); + } + + sb.Length -= 2; // trailing ', ' + sb.AppendLine(")"); + } + + return sb.ToString(); + + case SqlDbType.Decimal: + case SqlDbType.Float: + return ((double)paramValue).ToString(CultureInfo.InvariantCulture).Replace("'", "''"); + + default: + return paramValue.ToString().Replace("'", "''"); + } + } + + private static List GetStructured(this SqlParameterCollection paramCollection) + { + List filtered = new List(); + foreach (SqlParameter p in paramCollection) + { + if (p.SqlDbType == SqlDbType.Structured) + filtered.Add(p); + } + + return filtered; + } + + #endregion + + /// + /// This method fills all parameters of the sqlcommand and displays it as a string which can be copy pasted in your DB management tool for debug purposes. + /// + /// The SqlCommand you want parsed as a full SQL string + /// + public static string CommandAsSql(this SqlCommand command) + { + var sql = new StringBuilder(); + + if (command.Connection != null) + sql.Append("use ").Append(command.Connection.Database).AppendLine(";"); + + foreach (SqlParameter strucParam in command.Parameters.GetStructured()) + sql.AppendLine(strucParam.ParameterValueForSQL()); + + switch (command.CommandType) + { + case CommandType.Text: //checking 1st, since if we use Text SQL Commands we'll probably be logging more of them that if we had them grouped in Stored Procedures + command.CommandAsSql_Text(sql); + break; + + case CommandType.StoredProcedure: + command.CommandAsSql_StoredProcedure(sql); + break; + } + + return sql.ToString(); + } + + private static void CommandAsSql_Text(this SqlCommand command, StringBuilder sql) + { + string query = command.CommandText; + + foreach (SqlParameter p in command.Parameters) + query = Regex.Replace(query, "\\B" + p.ParameterName + "\\b", p.ParameterValueForSQL()); //the first one is \B, the 2nd one is \b, since ParameterName starts with @ which is a non-word character in RegEx (see https://stackoverflow.com/a/2544661) + + sql.AppendLine(query); + } + + private static void CommandAsSql_StoredProcedure(this SqlCommand command, StringBuilder sql) + { + sql.AppendLine("declare @return_value int;"); + + foreach (SqlParameter sp in command.Parameters) + { + if ((sp.Direction == ParameterDirection.InputOutput) || (sp.Direction == ParameterDirection.Output)) + { + sql.Append("declare ").Append(sp.ParameterName).Append("\t").Append(sp.SqlDbType.ToString()).Append("\t= "); + + sql.Append((sp.Direction == ParameterDirection.Output) ? "null" : sp.ParameterValueForSQL()).AppendLine(";"); + } + } + + sql.Append("exec [").Append(command.CommandText).AppendLine("]"); + + bool FirstParam = true; + foreach (SqlParameter param in command.Parameters) + { + if (param.Direction != ParameterDirection.ReturnValue) + { + sql.Append((FirstParam) ? "\t" : "\t, "); + + if (FirstParam) + FirstParam = false; + + if (param.Direction == ParameterDirection.Input) + { + if (param.SqlDbType != SqlDbType.Structured) + sql.Append(param.ParameterName).Append(" = ").AppendLine(param.ParameterValueForSQL()); + else + sql.Append(param.ParameterName).Append(" = ").AppendLine(param.ParameterName); + } + else + { + sql.Append(param.ParameterName).Append(" = ").Append(param.ParameterName).AppendLine(" output"); + } + } + } + sql.AppendLine(";"); + + sql.AppendLine("select 'Return Value' = convert(varchar, @return_value);"); + + foreach (SqlParameter sp in command.Parameters) + { + if ((sp.Direction == ParameterDirection.InputOutput) || (sp.Direction == ParameterDirection.Output)) + sql.Append("select '").Append(sp.ParameterName).Append("' = convert(varchar, ").Append(sp.ParameterName).AppendLine(");"); + } + } + } +} diff --git a/XUnitTestCommandAsSql/UnitTest1.cs b/XUnitTestCommandAsSql/UnitTest1.cs index 49cdb53..419a8a4 100644 --- a/XUnitTestCommandAsSql/UnitTest1.cs +++ b/XUnitTestCommandAsSql/UnitTest1.cs @@ -1,13 +1,13 @@ +using System; +using System.Data.SqlClient; +using CommandAsSql.System; +using Xunit; + /// /// Unit test for the CommandAsSql lib /// namespace XUnitTestCommandAsSql { - using System; - using System.Data.SqlClient; - using CommandAsSql; - using Xunit; - public class UnitTest1 { /// diff --git a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj index e81ac0a..7d8593d 100644 --- a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj +++ b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj @@ -7,12 +7,12 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers From fbb6cbbf8437b0227be82222ef2c4b6abaedfbaa Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:24:35 -0500 Subject: [PATCH 2/7] Rearranging source files. --- .../ExtensionMethods.cs} | 0 CommandAsSql/{ => System}/ExtensionMethods.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CommandAsSql/{ExtensionMethodsMicrosoft.cs => Microsoft/ExtensionMethods.cs} (100%) rename CommandAsSql/{ => System}/ExtensionMethods.cs (100%) diff --git a/CommandAsSql/ExtensionMethodsMicrosoft.cs b/CommandAsSql/Microsoft/ExtensionMethods.cs similarity index 100% rename from CommandAsSql/ExtensionMethodsMicrosoft.cs rename to CommandAsSql/Microsoft/ExtensionMethods.cs diff --git a/CommandAsSql/ExtensionMethods.cs b/CommandAsSql/System/ExtensionMethods.cs similarity index 100% rename from CommandAsSql/ExtensionMethods.cs rename to CommandAsSql/System/ExtensionMethods.cs From a2bf4641282c1bcd829d72093b7ef850954c94ac Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:31:48 -0500 Subject: [PATCH 3/7] Rearranging unit tests and adding test for Microsoft.Data.SqlClient. --- XUnitTestCommandAsSql/Microsoft/UnitTest1.cs | 48 +++++++++++++++++++ .../{ => System}/UnitTest1.cs | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 XUnitTestCommandAsSql/Microsoft/UnitTest1.cs rename XUnitTestCommandAsSql/{ => System}/UnitTest1.cs (97%) diff --git a/XUnitTestCommandAsSql/Microsoft/UnitTest1.cs b/XUnitTestCommandAsSql/Microsoft/UnitTest1.cs new file mode 100644 index 0000000..b65136c --- /dev/null +++ b/XUnitTestCommandAsSql/Microsoft/UnitTest1.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Data.SqlClient; +using CommandAsSql.Microsoft; +using Xunit; + +/// +/// Unit test for the CommandAsSql lib +/// +namespace XUnitTestCommandAsSql.Microsoft +{ + public class UnitTest1 + { + /// + /// Test a regular sql string with an int parameter + /// + [Fact(DisplayName = "Compare Command With Int Parameter")] + public void Compare_Command_With_Int_Parameter() + { + // arrange + const string Expected = "select * from products where productid = 1"; + var sc = new SqlCommand("select * from products where productid = @id"); + sc.Parameters.AddWithValue("@id", 1); // if you forget the @ here, the test fails. Give your thoughts about this in the GitHub issues section please. + + // act + var c = sc.CommandAsSql().Replace(Environment.NewLine, string.Empty); // not too happy about removing the newline + + // assert + Assert.Equal(Expected, c); + } + + [Fact(DisplayName = "Compare Command With String Parameter")] + public void Compare_Command_With_String_Parameter() + { + // arrange + const string Expected = "select * from products where productname = 'myname'"; + var sc = new SqlCommand("select * from products where productname = @name"); + sc.Parameters.AddWithValue("@name", "myname"); // if you forget the @ here, the test fails. Give your thoughts about this in the GitHub issues section please. + + // act + var c = sc.CommandAsSql().Replace(Environment.NewLine, string.Empty); // not too happy about removing the newline + + // assert + Assert.Equal(Expected, c); + } + + // todo: add more tests for structured, stored proc etc. + } +} diff --git a/XUnitTestCommandAsSql/UnitTest1.cs b/XUnitTestCommandAsSql/System/UnitTest1.cs similarity index 97% rename from XUnitTestCommandAsSql/UnitTest1.cs rename to XUnitTestCommandAsSql/System/UnitTest1.cs index 419a8a4..50d5806 100644 --- a/XUnitTestCommandAsSql/UnitTest1.cs +++ b/XUnitTestCommandAsSql/System/UnitTest1.cs @@ -6,7 +6,7 @@ /// /// Unit test for the CommandAsSql lib /// -namespace XUnitTestCommandAsSql +namespace XUnitTestCommandAsSql.System { public class UnitTest1 { From 830df9a72a5db644426aa1b892ece81725f652c1 Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:34:40 -0500 Subject: [PATCH 4/7] Reverting package version to latest stable. --- XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj index 7d8593d..7270590 100644 --- a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj +++ b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj @@ -7,7 +7,7 @@ - + From 8d7f009aeb6893712fc701e7b353ea77da9fa5fa Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:32:07 -0500 Subject: [PATCH 5/7] Updating release notes in preparation for a version 1.1.0 release. --- CommandAsSql/CommandAsSql.csproj | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CommandAsSql/CommandAsSql.csproj b/CommandAsSql/CommandAsSql.csproj index 8637e63..0dabff0 100644 --- a/CommandAsSql/CommandAsSql.csproj +++ b/CommandAsSql/CommandAsSql.csproj @@ -10,15 +10,14 @@ SqlCommand String ToString SqlClient Displays a SqlCommand as a full SQL string with all parameters inline. Makes it easy to debug (copy paste to db tool). Please submit pull requests on GitHub. jphellemons, flapper - 1.0.10 + 1.1.0-alpha.3 MIT true - - 1.0.10 included icon and changed ref to license and added readme.md to package (for nuget.org) - 1.0.9 added readme.md in package - 1.0.8 changed github action and upgraded sqlclient dependency - 1.0.3 Added the MIT license to both the nuget and the github repo - + 1.1.0 - Added support for Microsoft.Data.SqlClient +1.0.10 - Included icon and changed ref to license and added readme.md to package (for nuget.org) +1.0.9 - Added readme.md in package +1.0.8 - Changed github action and upgraded sqlclient dependency +1.0.3 - Added the MIT license to both the nuget and the github repo From a6587ae714fbc0b1f5cbc2cdd2636118d700dc48 Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:29:16 -0500 Subject: [PATCH 6/7] Updated package versions to latest. --- CommandAsSql/CommandAsSql.csproj | 2 +- XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CommandAsSql/CommandAsSql.csproj b/CommandAsSql/CommandAsSql.csproj index 0dabff0..bdc18f5 100644 --- a/CommandAsSql/CommandAsSql.csproj +++ b/CommandAsSql/CommandAsSql.csproj @@ -25,7 +25,7 @@ - + diff --git a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj index 7270590..f9130dc 100644 --- a/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj +++ b/XUnitTestCommandAsSql/XUnitTestCommandAsSql.csproj @@ -7,12 +7,12 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers From b1e5aa0b08a29090bb4e8b924c1fd429e6c9bc28 Mon Sep 17 00:00:00 2001 From: bmasephol <12200343+bmasephol@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:38:34 -0500 Subject: [PATCH 7/7] Bumping package version. --- CommandAsSql/CommandAsSql.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommandAsSql/CommandAsSql.csproj b/CommandAsSql/CommandAsSql.csproj index bdc18f5..d613857 100644 --- a/CommandAsSql/CommandAsSql.csproj +++ b/CommandAsSql/CommandAsSql.csproj @@ -10,7 +10,7 @@ SqlCommand String ToString SqlClient Displays a SqlCommand as a full SQL string with all parameters inline. Makes it easy to debug (copy paste to db tool). Please submit pull requests on GitHub. jphellemons, flapper - 1.1.0-alpha.3 + 1.1.0-alpha.4 MIT true 1.1.0 - Added support for Microsoft.Data.SqlClient