diff --git a/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.csproj b/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.csproj index fd032ac..78af237 100644 --- a/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.csproj +++ b/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.csproj @@ -1,55 +1,59 @@  - - net461;netstandard2.0;net5.0 - Rick Drizin - MIT - https://github.com/Drizin/DapperQueryBuilder/ - Dapper Query Builder using Fluent API and String Interpolation - Rick Drizin - Rick Drizin - 1.2.9 - false - DapperQueryBuilder (Strong Named) - DapperQueryBuilder.StrongName - DapperQueryBuilder.StrongName.xml - dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string - true - true - - NuGetReadMe.md - DapperQueryBuilder.StrongName - + + netstandard2.0;net462;net472;net5.0;net6.0;net7.0 + Rick Drizin + MIT + https://github.com/Drizin/DapperQueryBuilder/ + Dapper Query Builder using Fluent API and String Interpolation + Rick Drizin + Rick Drizin + 2.0.0-beta1 + false + DapperQueryBuilder (Strong Named) + DapperQueryBuilder.StrongName + DapperQueryBuilder.StrongName.xml + dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string + true + true + + NuGetReadMe.md + DapperQueryBuilder.StrongName + enable + 8.0 + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - True - ..\debug.snk - - - - - True - ..\release.snk - - - + + + + True + ..\debug.snk + + + + + True + ..\release.snk + + + - - - - + + + + diff --git a/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.nuspec b/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.nuspec index 97808c8..59823ca 100644 --- a/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.nuspec +++ b/src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.nuspec @@ -13,8 +13,4 @@ - - - - \ No newline at end of file diff --git a/src/DapperQueryBuilder.Tests/CommandBuilderTests.cs b/src/DapperQueryBuilder.Tests/CommandBuilderTests.cs index f73526c..8e6b368 100644 --- a/src/DapperQueryBuilder.Tests/CommandBuilderTests.cs +++ b/src/DapperQueryBuilder.Tests/CommandBuilderTests.cs @@ -1,4 +1,4 @@ -using Dapper; +using InterpolatedSql; using NUnit.Framework; using System; using System.Collections; @@ -19,7 +19,7 @@ public class CommandBuilderTests public CommandBuilderTests() { } // nunit requires parameterless constructor public CommandBuilderTests(bool reuseIdenticalParameters) { - DapperQueryBuilderOptions.ReuseIdenticalParameters = reuseIdenticalParameters; + InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters = reuseIdenticalParameters; } #region Setup @@ -49,12 +49,13 @@ public void TestBareCommand() int subCategoryId = 12; var query = cn - .QueryBuilder($@" + .QueryBuilder($$""" SELECT * FROM [Production].[Product] WHERE - [Name] LIKE {productName} - AND [ProductSubcategoryID] = {subCategoryId} - ORDER BY [ProductId]"); + [Name] LIKE {{productName}} + AND [ProductSubcategoryID] = {{subCategoryId}} + ORDER BY [ProductId] + """); Assert.AreEqual(@" SELECT * FROM [Production].[Product] @@ -74,12 +75,13 @@ public void TestNameof() int subCategoryId = 12; var query = cn - .QueryBuilder($@" + .QueryBuilder($$""" SELECT * FROM [Production].[Product] WHERE - [{nameof(Product.Name):raw}] LIKE {productName} - AND [ProductSubcategoryID] = {subCategoryId} - ORDER BY [ProductId]"); + [{{nameof(Product.Name):raw}}] LIKE {{productName}} + AND [ProductSubcategoryID] = {{subCategoryId}} + ORDER BY [ProductId] + """); Assert.AreEqual(@" SELECT * FROM [Production].[Product] @@ -171,7 +173,9 @@ public void TestStoredProcedureOutput() .AddParameter("Input1", dbType: DbType.Int32); //.AddParameter("Output1", dbType: DbType.Int32, direction: ParameterDirection.Output); //var getter = ParameterInfos.GetSetter((MyPoco p) => p.MyValue); - cmd.Parameters.Add(ParameterInfo.CreateOutputParameter("Output1", poco, p => p.MyValue, ParameterInfo.OutputParameterDirection.Output, size: 4)); + var outputParm = new DbTypeParameterInfo("Output1", size: 4); + outputParm.ConfigureOutputParameter(poco, p => p.MyValue, SqlParameterInfo.OutputParameterDirection.Output); + cmd.AddParameter(outputParm); //TODO: AddOutputParameter? move ConfigureOutputParameter inside it. // previously this was cmd.Parameters.Add, but not Parameters is get-only int affected = cmd.Execute(commandType: CommandType.StoredProcedure); string outputValue = cmd.Parameters.Get("Output1"); // why is this being returned as string? just because I didn't provide type above? @@ -304,10 +308,11 @@ public void TestOperatorOverload() public void TestAutospacing2() { string search = "%mountain%"; - var cmd = cn.CommandBuilder($@" + var cmd = cn.CommandBuilder($$""" SELECT * FROM [Production].[Product] - WHERE [Name] LIKE {search} - AND 1=2"); + WHERE [Name] LIKE {{search}} + AND 1=2 + """); Assert.AreEqual( "SELECT * FROM [Production].[Product]" + Environment.NewLine + "WHERE [Name] LIKE @p0" + Environment.NewLine + @@ -319,10 +324,11 @@ public void TestAutospacing3() { string productNumber = "EC-M092"; int productId = 328; - var cmd = cn.CommandBuilder($@" + var cmd = cn.CommandBuilder($$""" UPDATE [Production].[Product] - SET [ProductNumber]={productNumber} - WHERE [ProductId]={productId}"); + SET [ProductNumber]={{productNumber}} + WHERE [ProductId]={{productId}} + """); string expected = "UPDATE [Production].[Product]" + Environment.NewLine + @@ -393,7 +399,7 @@ public void TestRepeatedParameters() query.Append($"OR [ProductCategoryID]={subCategoryId}"); query.Append($"OR [ProductCategoryID]={categoryId})"); - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) { Assert.AreEqual(@"SELECT * FROM [table1] WHERE ([Name]=@p0 or [Author]=@p0" + " or [Creator]=@p0)" @@ -425,7 +431,7 @@ public void TestRepeatedParameters() [Test] public void TestRepeatedParameters2() { - if (!DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (!InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) return; int? fileId = null; @@ -525,7 +531,7 @@ public void TestRepeatedParameters4() qb.Append($"{"A"}"); // @p21 should reuse @p0 qb.Append($"{"B"}"); // @p22 should reuse @p1 - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) Assert.AreEqual("@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,@p11,@p12,@p13,@p14,@p14,@p15,@p16,@p17,@p18,@p19,@p20,@p0 @p1", qb.Sql); else Assert.AreEqual("@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,@p20,@p21,@p22 @p23", qb.Sql); @@ -562,7 +568,7 @@ public void TestRepeatedParameters5() qb.Append($"{"A"}"); // @p20 should reuse @p0 qb.Append($"{"B"}"); // @p21 should reuse @p1 - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) Assert.AreEqual("@p0 @p1 @p2 @p3 @p4 @p5 @p6 @p7 @p8 @p9 @p10 @p11 @p12 @p13 @p14 @p15 @p16 @p17 @p18 @p19 @p0 @p1", qb.Sql); else Assert.AreEqual("@p0 @p1 @p2 @p3 @p4 @p5 @p6 @p7 @p8 @p9 @p10 @p11 @p12 @p13 @p14 @p15 @p16 @p17 @p18 @p19 @p20 @p21", qb.Sql); @@ -585,7 +591,7 @@ public void TestMultipleStatements() cmd.Append($"DELETE FROM Orders WHERE OrderId = {orderId}; "); cmd.Append($"INSERT INTO Logs (Action, UserId, Description) VALUES ({action}, {orderId}, {description}); "); - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) { Assert.AreEqual(cmd.Parameters.Count, 3); Assert.AreEqual(cmd.Parameters.Get("p0"), orderId); @@ -691,7 +697,7 @@ declare @v23 nvarchar(10)={v} var s = query.Sql; var p = query.Parameters; - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) + if (InterpolatedSqlBuilder.DefaultOptions.ReuseIdenticalParameters) { Assert.AreEqual(@" declare @v1 nvarchar(10)=@p0 @@ -719,7 +725,7 @@ declare @v21 nvarchar(10)=@p0 declare @v22 nvarchar(10)=@p0 declare @v23 nvarchar(10)=@p0 select 'ok' -".TrimStart(), query.Sql); +", query.Sql); Assert.AreEqual(query.Parameters.Get("p0"), v); Assert.AreEqual(query.Parameters.Get>("parray1"), numList); @@ -752,7 +758,7 @@ declare @v21 nvarchar(10)=@p21 declare @v22 nvarchar(10)=@p22 declare @v23 nvarchar(10)=@p23 select 'ok' -".TrimStart(), query.Sql); +", query.Sql); Assert.AreEqual(query.Parameters.Get("p0"), v); Assert.AreEqual(query.Parameters.Get("p1"), v); @@ -784,7 +790,6 @@ @BusinessEntityID [int] EXEC [dbo].[uspGetEmployeeManagers] @BusinessEntityID = @BusinessEntityID END").Execute(); - var q = cn.CommandBuilder($"[dbo].[uspGetEmployeeManagers_Twice]") .AddParameter("BusinessEntityID", 280); diff --git a/src/DapperQueryBuilder.Tests/DapperQueryBuilder.Tests.csproj b/src/DapperQueryBuilder.Tests/DapperQueryBuilder.Tests.csproj index 4c838d7..ae82d8a 100644 --- a/src/DapperQueryBuilder.Tests/DapperQueryBuilder.Tests.csproj +++ b/src/DapperQueryBuilder.Tests/DapperQueryBuilder.Tests.csproj @@ -13,6 +13,7 @@ MIT https://github.com/Drizin/DapperQueryBuilder/ + 11.0 diff --git a/src/DapperQueryBuilder.Tests/FluentQueryBuilderTests.cs b/src/DapperQueryBuilder.Tests/FluentQueryBuilderTests.cs index 4e899ad..79062bf 100644 --- a/src/DapperQueryBuilder.Tests/FluentQueryBuilderTests.cs +++ b/src/DapperQueryBuilder.Tests/FluentQueryBuilderTests.cs @@ -1,3 +1,4 @@ +using InterpolatedSql; using NUnit.Framework; using System; using System.Collections.Generic; @@ -22,8 +23,7 @@ public void Setup() string expected = @"SELECT ProductId, Name, ListPrice, Weight FROM [Production].[Product] WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2 -ORDER BY ProductId -"; +ORDER BY ProductId"; public class Product { @@ -112,8 +112,7 @@ FROM [Production].[Product] p LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID] WHERE p.[ListPrice] BETWEEN @p0 and @p1 AND cat.[Name] IS NOT NULL GROUP BY cat.[Name], sc.[Name] -HAVING COUNT(*)>@p2 -"; +HAVING COUNT(*)>@p2"; Assert.AreEqual(expected, q.Sql); @@ -134,8 +133,7 @@ public void TestAndOr() string expected = @"SELECT ProductId, Name, ListPrice, Weight FROM [Production].[Product] WHERE [ListPrice] <= @p0 AND ([Weight] <= @p1 OR [Name] LIKE @p2) -ORDER BY ProductId -"; +ORDER BY ProductId"; var q = cn.FluentQueryBuilder() .Select($"ProductId") @@ -174,8 +172,7 @@ public void TestAndOr2() string expected = @"SELECT ProductId, Name, ListPrice, Weight FROM [Production].[Product] -WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3) -"; +WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"; var q = cn.FluentQueryBuilder() .Select($"ProductId, Name, ListPrice, Weight") @@ -226,6 +223,12 @@ public void TestDetachedFilters() string where = filters.BuildFilters(parms); Assert.AreEqual(@"WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)", where); + + Assert.AreEqual(4, parms.ParameterNames.Count()); + Assert.AreEqual(minPrice, parms.Get("p0")); + Assert.AreEqual(maxPrice, parms.Get("p1")); + Assert.AreEqual(maxWeight, parms.Get("p2")); + Assert.AreEqual(search, parms.Get("p3")); } [Test] @@ -262,8 +265,7 @@ FROM [Production].[Product] p WHERE p.[ListPrice] BETWEEN @p0 and @p1 AND cat.[Name] IS NOT NULL GROUP BY cat.[Name] HAVING COUNT(*)>@p2 -ORDER BY cat.[Name] -"; +ORDER BY cat.[Name]"; Assert.AreEqual(expected, q.Sql); @@ -291,8 +293,7 @@ FROM [Production].[Product] p LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID] GROUP BY cat.[Name] -HAVING COUNT(*)>@p0 -"; +HAVING COUNT(*)>@p0"; Assert.AreEqual(expected, q.Sql); @@ -332,11 +333,9 @@ public void FluentQueryBuilderInsideCommandBuilder() string expected = @"SELECT * FROM [Sales].[SalesOrderDetail] WHERE [ProductId] IN (SELECT ProductId FROM [Production].[Product] -WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2 -) AND [SalesOrderId] IN (SELECT SalesOrderID +WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2) AND [SalesOrderId] IN (SELECT SalesOrderID FROM [Sales].[SalesOrderHeader] -WHERE [CustomerId] = @p3 AND [Status] IN @parray4 -)"; +WHERE [CustomerId] = @p3 AND [Status] IN @parray4)"; Assert.AreEqual(expected, finalQuery.Sql); Assert.That(finalQuery.Parameters.ParameterNames.Contains("p0")); diff --git a/src/DapperQueryBuilder.Tests/PostgreSQLTests.cs b/src/DapperQueryBuilder.Tests/PostgreSQLTests.cs index f454c8b..dcbc732 100644 --- a/src/DapperQueryBuilder.Tests/PostgreSQLTests.cs +++ b/src/DapperQueryBuilder.Tests/PostgreSQLTests.cs @@ -1,11 +1,8 @@ -using Npgsql; +using InterpolatedSql; +using Npgsql; using NUnit.Framework; -using System; -using System.Collections.Generic; using System.Data; -using System.Data.OleDb; using System.Linq; -using System.Text; namespace DapperQueryBuilder.Tests { @@ -23,8 +20,8 @@ public void Setup() public void TearDown() { // reverting back for next unit tests - DapperQueryBuilderOptions.DatabaseParameterSymbol = "@"; - DapperQueryBuilderOptions.AutoGeneratedParameterName = "p"; + InterpolatedSqlBuilder.DefaultOptions.DatabaseParameterSymbol = "@"; + InterpolatedSqlBuilder.DefaultOptions.AutoGeneratedParameterPrefix = "p"; } #endregion @@ -56,8 +53,8 @@ public void TestParameters() public void TestAutoGeneratedParameterPrefix() { // Npgsql does NOT require this, but it may be required in some databases/drivers which do not accept "at-parameters" (@p0, @p1, etc). - DapperQueryBuilderOptions.DatabaseParameterSymbol = ":"; - DapperQueryBuilderOptions.AutoGeneratedParameterName = "parm"; + InterpolatedSqlBuilder.DefaultOptions.DatabaseParameterSymbol = ":"; + InterpolatedSqlBuilder.DefaultOptions.AutoGeneratedParameterPrefix = "parm"; string search = "%Dinosaur%"; var cmd = cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}"); diff --git a/src/DapperQueryBuilder.Tests/QueryBuilderTests.cs b/src/DapperQueryBuilder.Tests/QueryBuilderTests.cs index fc5c852..f37ecf5 100644 --- a/src/DapperQueryBuilder.Tests/QueryBuilderTests.cs +++ b/src/DapperQueryBuilder.Tests/QueryBuilderTests.cs @@ -1,6 +1,6 @@ +using InterpolatedSql; using NUnit.Framework; using System; -using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; @@ -107,6 +107,12 @@ public void TestDetachedFilters() string where = filters.BuildFilters(parms); Assert.AreEqual(@"WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)", where); + + Assert.AreEqual(4, parms.ParameterNames.Count()); + Assert.AreEqual(minPrice, parms.Get("p0")); + Assert.AreEqual(maxPrice, parms.Get("p1")); + Assert.AreEqual(maxWeight, parms.Get("p2")); + Assert.AreEqual(search, parms.Get("p3")); } [Test] diff --git a/src/DapperQueryBuilder/CommandBuilder.cs b/src/DapperQueryBuilder/CommandBuilder.cs deleted file mode 100644 index c155e9e..0000000 --- a/src/DapperQueryBuilder/CommandBuilder.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace DapperQueryBuilder -{ - /// - /// CommandBuilder wraps an underlying SQL statement and the associated parameters.
- /// Allows to easily add new clauses to underlying statement and also add new parameters. - ///
- [DebuggerDisplay("{Sql} ({_parametersStr,nq})")] - public class CommandBuilder : ICompleteCommand - { - #region Members - private readonly IDbConnection _cnn; - private readonly ParameterInfos _parameters; - private string _parametersStr; - - private readonly StringBuilder _command; - - /// - public IDbConnection Connection { get { return _cnn; } } - - #endregion - - #region statics/constants - - /// - /// Identify all types of line-breaks - /// - protected static readonly Regex _lineBreaksRegex = new Regex(@"(\r\n|\n|\r)", RegexOptions.Compiled); - - #endregion - - #region ctors - /// - /// New CommandBuilder. - /// - /// - public CommandBuilder(IDbConnection cnn) - { - _cnn = cnn; - _command = new StringBuilder(); - _parameters = new ParameterInfos(); - } - - /// - /// New CommandBuilder based on an initial command.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. - ///
- /// - /// SQL command - public CommandBuilder(IDbConnection cnn, FormattableString command) : this(cnn) - { - var parsedStatement = new InterpolatedStatementParser(command); - parsedStatement.MergeParameters(this.Parameters); - string sql = AdjustMultilineString(parsedStatement.Sql); - _command.Append(sql); - } - #endregion - - #region Parameters Adding/Merging - /// - /// Adds single parameter to current Command Builder.
- ///
- public CommandBuilder AddParameter(string parameterName, object parameterValue = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null) - { - _parameters.Add(new ParameterInfo(parameterName, parameterValue, dbType, direction, size, precision, scale)); - _parametersStr = string.Join(", ", _parameters.ParameterNames.ToList().Select(n => DapperQueryBuilderOptions.DatabaseParameterSymbol + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - return this; - } - - - /// - /// Adds all public properties of an object (like a POCO) as parameters of the current Command Builder.
- /// This is like Dapper templates: useful when you're passing an object with multiple properties and you'll reference those properties in the SQL statement.
- /// This method does not check for name clashes against previously added parameters.
- ///
- public void AddObjectProperties(object obj) - { - Dictionary props = - obj.GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .ToDictionary(prop => prop.Name, prop => prop); - - foreach (var prop in props) - { - _parameters.Add(new ParameterInfo(prop.Key, prop.Value.GetValue(obj, new object[] { }))); - } - _parametersStr = string.Join(", ", _parameters.ParameterNames.ToList().Select(n => DapperQueryBuilderOptions.DatabaseParameterSymbol + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - } - #endregion - - - - /// - /// Appends a statement to the current command.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. - ///
- /// SQL command - public CommandBuilder Append(FormattableString statement) - { - var parsedStatement = new InterpolatedStatementParser(statement); - parsedStatement.MergeParameters(this.Parameters); - string sql = AdjustMultilineString(parsedStatement.Sql); - if (!string.IsNullOrWhiteSpace(sql)) - { - // we assume that a single word will always be appended in a single statement (why would anyone split a single sql word in 2 appends?!), - // so if there is no whitespace (or line break) between last text and new text, we add a space betwen them - string currentLine = _command.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).LastOrDefault(); - if (currentLine != null && currentLine.Length > 0 && !char.IsWhiteSpace(currentLine.Last()) && currentLine.Last()!=',' && !char.IsWhiteSpace(sql[0])) - _command.Append(" "); - } - _command.Append(sql); - return this; - } - - /// - /// Appends a statement to the current command.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. - ///
- public static CommandBuilder operator + (CommandBuilder cmd, FormattableString fs) - { - return cmd.Append(fs); - } - - /// - /// Replaces a text by a replacement text
- ///
- public CommandBuilder Replace(string oldValue, FormattableString newValue) - { - var parsedStatement = new InterpolatedStatementParser(newValue); - parsedStatement.MergeParameters(this.Parameters); - string sql = AdjustMultilineString(parsedStatement.Sql); - _command.Replace(oldValue, sql); - return this; - } - - - - - - - #region Multi-line blocks can be conveniently used with any indentation, and we will correctly adjust the indentation of those blocks (TrimLeftPadding and TrimFirstEmptyLine) - /// - /// Given a text block (multiple lines), this removes the left padding of the block, by calculating the minimum number of spaces which happens in EVERY line. - /// Then, other methods writes the lines one by one, which in case will respect the current indent of the writer. - /// - protected string AdjustMultilineString(string block) - { - // copied from https://github.com/Drizin/CodegenCS/ - - if (string.IsNullOrEmpty(block)) - return null; - string[] parts = _lineBreaksRegex.Split(block); - if (parts.Length <= 1) // no linebreaks at all - return block; - var nonEmptyLines = parts.Where(line => line.TrimEnd().Length > 0).ToList(); - if (nonEmptyLines.Count <= 1) // if there's not at least 2 non-empty lines, assume that we don't need to adjust anything - return block; - - Match m = _lineBreaksRegex.Match(block); - if (m != null && m.Success && m.Index == 0) - { - block = block.Substring(m.Length); // remove first empty line - parts = _lineBreaksRegex.Split(block); - nonEmptyLines = parts.Where(line => line.TrimEnd().Length > 0).ToList(); - } - - - int minNumberOfSpaces = nonEmptyLines.Select(nonEmptyLine => nonEmptyLine.Length - nonEmptyLine.TrimStart().Length).Min(); - - StringBuilder sb = new StringBuilder(); - - var matches = _lineBreaksRegex.Matches(block); - int lastPos = 0; - for (int i = 0; i < matches.Count; i++) - { - string line = block.Substring(lastPos, matches[i].Index - lastPos); - string lineBreak = block.Substring(matches[i].Index, matches[i].Length); - lastPos = matches[i].Index + matches[i].Length; - - sb.Append(line.Substring(Math.Min(line.Length, minNumberOfSpaces))); - sb.Append(lineBreak); - } - string lastLine = block.Substring(lastPos); - sb.Append(lastLine.Substring(Math.Min(lastLine.Length, minNumberOfSpaces))); - - return sb.ToString(); - } - #endregion - - - /// - /// Appends a statement to the current command, but before statement adds a linebreak.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. - ///
- /// SQL command - public CommandBuilder AppendLine(FormattableString statement) - { - // instead of appending line AFTER the statement it makes sense to add BEFORE, just to ISOLATE the new line from previous one - // there's no point in having linebreaks at the end of a query - _command.AppendLine(); - - this.Append(statement); - return this; - } - - - /// - /// SQL of Command - /// - public virtual string Sql => _command.ToString(); // base CommandBuilder will just have a single variable for the statement; - - /// - /// Parameters of Command - /// - public virtual ParameterInfos Parameters => _parameters; - - } -} diff --git a/src/DapperQueryBuilder/Dapper-QueryBuilder.nuspec b/src/DapperQueryBuilder/Dapper-QueryBuilder.nuspec index 04055c1..206d97e 100644 --- a/src/DapperQueryBuilder/Dapper-QueryBuilder.nuspec +++ b/src/DapperQueryBuilder/Dapper-QueryBuilder.nuspec @@ -13,8 +13,4 @@ - - - - \ No newline at end of file diff --git a/src/DapperQueryBuilder/DapperQueryBuilder.csproj b/src/DapperQueryBuilder/DapperQueryBuilder.csproj index 355db0c..de50458 100644 --- a/src/DapperQueryBuilder/DapperQueryBuilder.csproj +++ b/src/DapperQueryBuilder/DapperQueryBuilder.csproj @@ -1,46 +1,50 @@  - - net461;netstandard2.0;net5.0 - Rick Drizin - MIT - https://github.com/Drizin/DapperQueryBuilder/ - Dapper Query Builder using Fluent API and String Interpolation - Rick Drizin - Rick Drizin - 1.2.9 - false - DapperQueryBuilder - Dapper-QueryBuilder - DapperQueryBuilder.xml - dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string - true - true - - NuGetReadMe.md - DapperQueryBuilder - + + netstandard2.0;net462;net472;net5.0;net6.0;net7.0 + Rick Drizin + MIT + https://github.com/Drizin/DapperQueryBuilder/ + Dapper Query Builder using Fluent API and String Interpolation + Rick Drizin + Rick Drizin + 2.0.0-beta1 + false + DapperQueryBuilder + Dapper-QueryBuilder + DapperQueryBuilder.xml + dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string + true + true + + NuGetReadMe.md + DapperQueryBuilder + enable + 8.0 + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/DapperQueryBuilder/DapperQueryBuilderOptions.cs b/src/DapperQueryBuilder/DapperQueryBuilderOptions.cs index b183eb3..614c6ca 100644 --- a/src/DapperQueryBuilder/DapperQueryBuilderOptions.cs +++ b/src/DapperQueryBuilder/DapperQueryBuilderOptions.cs @@ -1,39 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; +using InterpolatedSql; namespace DapperQueryBuilder { /// - /// Global options for DapperQueryBuilder + /// Global Options /// public class DapperQueryBuilderOptions { /// - /// In the rendered SQL statement the parameters by default are named like @p0, @p1, etc.
- /// You can change the name p0/p1/etc to any other prfix.
- /// Example: if you set to "arg" you'll get @arg0, @arg1, etc.
+ /// Responsible for parsing SqlParameters (see ) + /// into a list of SqlParameterInfo that ///
- public static string AutoGeneratedParameterName { get; set; } = "p"; - - /// - /// String that is appended to the parameter name for enumerable types to avoid name conflicts. - /// - public static string ParameterArrayNameSuffix { get; set; } = "array"; - - /// - /// In the rendered SQL statement the parameters by default are named like @p0, @p1, etc.
- /// If your database does not accept @ symbol you can change for any other symbol.
- /// For Oracle you should use ":"
- ///
- public static string DatabaseParameterSymbol { get; set; } = "@"; - - - /// - /// If enabled (default is disabled) each added parameter will check if identical parameter (same type and value) - /// was already added, and if so will reuse the existing parameter. - /// - public static bool ReuseIdenticalParameters { get; set; } = false; - + public static SqlParameterMapper InterpolatedSqlParameterParser = new SqlParameterMapper(); } } diff --git a/src/DapperQueryBuilder/Filter.cs b/src/DapperQueryBuilder/Filter.cs deleted file mode 100644 index e6ef974..0000000 --- a/src/DapperQueryBuilder/Filter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; - -namespace DapperQueryBuilder -{ - /// - /// Filter statement defined in a single statement
- /// It can include multiple conditions (if defined in a single statement during constructor),
- /// but usually this is used as one condition (one column, one comparison operator, and one parameter). - ///
- [DebuggerDisplay("{Sql} ({_parametersStr,nq})")] - public class Filter : IFilter - { - #region Members - /// - /// Formatted SQL statement using parameters (@p0, @p1, etc) - /// - public string Sql { get; set; } - - /// - /// Dictionary of Dapper parameters - /// - public ParameterInfos Parameters { get; set; } - - private string _parametersStr; - #endregion - - #region ctor - /// - /// New Filter statement.
- /// Example: $"[CategoryId] = {categoryId}"
- /// Example: $"[Name] LIKE {productName}" - ///
- public Filter(FormattableString filter) - { - var parsedStatement = new InterpolatedStatementParser(filter); - Sql = parsedStatement.Sql; - Parameters = parsedStatement.Parameters; - _parametersStr = string.Join(", ", Parameters.ParameterNames.ToList().Select(n => DapperQueryBuilderOptions.DatabaseParameterSymbol + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - } - #endregion - - #region IFilter - /// - public void WriteFilter(StringBuilder sb) - { - sb.Append(Sql); - } - - /// - public void MergeParameters(ParameterInfos target) - { - string newSql = target.MergeParameters(Parameters, Sql); - if (newSql != null) - { - Sql = newSql; - //_parametersStr = string.Join(", ", Parameters.ParameterNames.ToList().Select(n => "@" + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - _parametersStr = string.Join(", ", Parameters.ParameterNames.ToList().Select(n => "'" + Convert.ToString(Parameters.Get(n)) + "'")); - // filter parameters in Sql were renamed and won't match the previous passed filters - discard original parameters to avoid reusing wrong values - Parameters = null; - } - } - #endregion - } -} diff --git a/src/DapperQueryBuilder/FilterExtensions.cs b/src/DapperQueryBuilder/FilterExtensions.cs new file mode 100644 index 0000000..0de9e81 --- /dev/null +++ b/src/DapperQueryBuilder/FilterExtensions.cs @@ -0,0 +1,27 @@ +using Dapper; +using InterpolatedSql; + +namespace DapperQueryBuilder +{ + public static class FilterExtensions + { + /// + /// If you're using Filters in standalone structure (without QueryBuilder),
+ /// you can just "build" the filters over a ParameterInfos and get the string for the filters (with leading WHERE) + ///
+ public static string BuildFilters(this Filters filters, DynamicParameters target) + { + ParametersDictionary parameters = new ParametersDictionary(); + foreach (var parameter in parameters.Values) + SqlParameterMapper.Default.AddToDynamicParameters(target, parameter); + + InterpolatedSqlBuilder command = new InterpolatedSqlBuilder(); + filters.WriteTo(command); + if (!command.IsEmpty) + command.InsertLiteral(0, "WHERE "); + foreach(var parameter in ParametersDictionary.LoadFrom(command)) + SqlParameterMapper.Default.AddToDynamicParameters(target, parameter.Value); + return command.Sql; + } + } +} diff --git a/src/DapperQueryBuilder/Filters.cs b/src/DapperQueryBuilder/Filters.cs deleted file mode 100644 index 3c84c01..0000000 --- a/src/DapperQueryBuilder/Filters.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; - -namespace DapperQueryBuilder -{ - /// - /// Multiple Filter statements which are grouped together. Can be grouped with ANDs or ORs. - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class Filters : List, IFilter - { - #region Members - /// - /// By default Filter Groups are combined with AND operator. But you can use OR. - /// - public FiltersType Type { get; set; } = FiltersType.AND; - - /// - /// How a list of Filters are combined (AND operator or OR operator) - /// - public enum FiltersType - { - /// - /// AND - /// - AND, - - /// - /// OR - /// - OR - } - #endregion - - #region ctor - - /// - /// Create a new group of filters. - /// - public Filters(FiltersType type, IEnumerable filters) - { - Type = type; - this.AddRange(filters); - } - - /// - /// Create a new group of filters which are combined with AND operator. - /// - public Filters(IEnumerable filters): this(FiltersType.AND, filters) - { - } - - /// - /// Create a new group of filters from formattable strings - /// - public Filters(FiltersType type, params FormattableString[] filters): - this(type, filters.Select(fiString => new Filter(fiString))) - { - } - - /// - /// Create a new group of filters from formattable strings which are combined with AND operator. - /// - public Filters(params FormattableString[] filters) : this(FiltersType.AND, filters) - { - } - #endregion - - #region IFilter - /// - public void WriteFilter(StringBuilder sb) - { - //if (this.Count() > 1) - // sb.Append("("); - for (int i = 0; i < this.Count(); i++) - { - if (i > 0 && Type == FiltersType.AND) - sb.Append(" AND "); - else if (i > 0 && Type == FiltersType.OR) - sb.Append(" OR "); - IFilter filter = this[i]; - if (filter is Filters && ((Filters)filter).Count() > 1) // only put brackets in groups after the first level - { - sb.Append("("); - filter.WriteFilter(sb); - sb.Append(")"); - } - else - filter.WriteFilter(sb); - } - //if (this.Count() > 1) - // sb.Append(")"); - } - - /// - public void MergeParameters(ParameterInfos target) - { - foreach(IFilter filter in this) - { - filter.MergeParameters(target); - } - } - - /// - /// If you're using Filters in standalone structure (without QueryBuilder),
- /// you can just "build" the filters over a ParameterInfos and get the string for the filters (with leading WHERE) - ///
- /// - /// - public string BuildFilters(DynamicParameters target) - { - ParameterInfos parameters = new ParameterInfos(); - foreach (IFilter filter in this) - { - filter.MergeParameters(parameters); - } - foreach (var parameter in parameters.Values) - target.Add(parameter.Name, parameter.Value, parameter.DbType, parameter.ParameterDirection, parameter.Size); - StringBuilder sb = new StringBuilder(); - WriteFilter(sb); - if (sb.Length > 0) - return "WHERE " + sb.ToString(); - return ""; - } - - private string DebuggerDisplay { get { StringBuilder sb = new StringBuilder(); sb.Append($"({this.Count()} filters): "); WriteFilter(sb); return sb.ToString(); } } - #endregion - - } -} diff --git a/src/DapperQueryBuilder/FluentQueryBuilder/FluentQueryBuilder.cs b/src/DapperQueryBuilder/FluentQueryBuilder/FluentQueryBuilder.cs index 9ad7484..2a84e1b 100644 --- a/src/DapperQueryBuilder/FluentQueryBuilder/FluentQueryBuilder.cs +++ b/src/DapperQueryBuilder/FluentQueryBuilder/FluentQueryBuilder.cs @@ -1,9 +1,7 @@ -using Dapper; +using InterpolatedSql; using System; -using System.Collections.Generic; using System.Data; using System.Linq; -using System.Text; using System.Text.RegularExpressions; namespace DapperQueryBuilder @@ -11,16 +9,13 @@ namespace DapperQueryBuilder /// /// FluentQueryBuilder allows to build queries using a Fluent-API interface /// - public class FluentQueryBuilder : IEmptyQueryBuilder, ISelectBuilder, ISelectDistinctBuilder, IFromBuilder, IWhereBuilder, IGroupByBuilder, IGroupByHavingBuilder, IOrderByBuilder, ICompleteCommand + public class FluentQueryBuilder : QueryBuilder, IEmptyQueryBuilder, ISelectBuilder, ISelectDistinctBuilder, IFromBuilder, IWhereBuilder, IGroupByBuilder, IGroupByHavingBuilder, IOrderByBuilder, ICompleteCommand { #region Members - private readonly QueryBuilder _queryBuilder; - private readonly List _selectColumns = new List(); - private readonly List _fromTables = new List(); - private readonly List _orderBy = new List(); - private readonly List _groupBy = new List(); - private readonly List _having = new List(); + private readonly InterpolatedSqlBuilder _orderBy = new InterpolatedSqlBuilder(); + private readonly InterpolatedSqlBuilder _groupBy = new InterpolatedSqlBuilder(); + private readonly InterpolatedSqlBuilder _having = new InterpolatedSqlBuilder(); private int? _rowCount = null; private int? _offset = null; private bool _isSelectDistinct = false; @@ -32,21 +27,16 @@ public class FluentQueryBuilder : IEmptyQueryBuilder, ISelectBuilder, ISelectDis /// Should be constructed using .Select(), .From(), .Where(), etc. /// /// - public FluentQueryBuilder(IDbConnection cnn) - { - _queryBuilder = new QueryBuilder(cnn); - } + public FluentQueryBuilder(IDbConnection cnn) : base(cnn) { } #endregion - #region Fluent API methods +#region Fluent API methods /// /// Adds one column to the select clauses /// - public ISelectBuilder Select(FormattableString column) + public new ISelectBuilder Select(FormattableString column) { - var parsedStatement = new InterpolatedStatementParser(column); - parsedStatement.MergeParameters(this.Parameters); - _selectColumns.Add(parsedStatement.Sql); + base.Select(column); return this; } @@ -67,9 +57,7 @@ public ISelectBuilder Select(params FormattableString[] moreColumns) public ISelectDistinctBuilder SelectDistinct(FormattableString select) { _isSelectDistinct = true; - var parsedStatement = new InterpolatedStatementParser(select); - parsedStatement.MergeParameters(this.Parameters); - _selectColumns.Add(parsedStatement.Sql); + base.Select(select); return this; } @@ -90,14 +78,13 @@ public ISelectDistinctBuilder SelectDistinct(params FormattableString[] moreColu /// You can add an alias after table name.
/// You can also add INNER JOIN, LEFT JOIN, etc (with the matching conditions). /// - public IFromBuilder From(FormattableString from) + public new IFromBuilder From(FormattableString from) { - var parsedStatement = new InterpolatedStatementParser(from); - parsedStatement.MergeParameters(this.Parameters); - string sql = parsedStatement.Sql; - if (!_fromTables.Any() && !Regex.IsMatch(sql, "\\b FROM \\b", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)) - sql = "FROM " + sql; - _fromTables.Add(sql); + var target = new InterpolatedSqlBuilder(); + base.Options.Parser.ParseAppend(from, target); + if (_froms.IsEmpty && !Regex.IsMatch(target.Sql, "\\b FROM \\b", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)) + target.InsertLiteral(0, "FROM "); + base.From((FormattableString)target); return this; } //TODO: create options with InnerJoin, LeftJoin, RightJoin, FullJoin, CrossJoin? Create overloads with table alias? @@ -108,9 +95,9 @@ public IFromBuilder From(FormattableString from) /// public IOrderByBuilder OrderBy(FormattableString orderBy) { - var parsedStatement = new InterpolatedStatementParser(orderBy); - parsedStatement.MergeParameters(this.Parameters); - _orderBy.Add(parsedStatement.Sql); + if (!_orderBy.IsEmpty) + _orderBy.AppendLiteral(", "); + _orderBy.Append(orderBy); return this; } @@ -119,9 +106,9 @@ public IOrderByBuilder OrderBy(FormattableString orderBy) /// public IGroupByBuilder GroupBy(FormattableString groupBy) { - var parsedStatement = new InterpolatedStatementParser(groupBy); - parsedStatement.MergeParameters(this.Parameters); - _groupBy.Add(parsedStatement.Sql); + if (!_groupBy.IsEmpty) + _groupBy.AppendLiteral(", "); + _groupBy.Append(groupBy); return this; } @@ -130,9 +117,9 @@ public IGroupByBuilder GroupBy(FormattableString groupBy) /// public IGroupByHavingBuilder Having(FormattableString having) { - var parsedStatement = new InterpolatedStatementParser(having); - parsedStatement.MergeParameters(this.Parameters); - _having.Add(parsedStatement.Sql); + if (!_having.IsEmpty) + _having.AppendLiteral(", "); + _having.Append(having); return this; } @@ -146,24 +133,24 @@ public ICompleteCommand Limit(int offset, int rowCount) return this; } - #endregion +#endregion - #region Where overrides +#region Where overrides /// /// Adds a new condition to where clauses. /// - public IWhereBuilder Where(Filter filter) + public new IWhereBuilder Where(Filter filter) { - _queryBuilder.Where(filter); + base.Where(filter); return this; } /// /// Adds a new condition to where clauses. /// - public IWhereBuilder Where(Filters filters) + public new IWhereBuilder Where(Filters filters) { - _queryBuilder.Where(filters); + base.Where(filters); return this; } @@ -172,64 +159,68 @@ public IWhereBuilder Where(Filters filters) /// Adds a new condition to where clauses.
/// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. /// - public IWhereBuilder Where(FormattableString filter) + public new IWhereBuilder Where(FormattableString filter) { - _queryBuilder.Where(filter); + base.Where(filter); return this; } - #endregion +#endregion +#region ICompleteCommand - #region ICompleteCommand +#region Sql - #region Sql /// - /// + /// Gets the combined command /// - public string Sql + public override InterpolatedSqlBuilder CombinedQuery { get { - //TODO: bool AutoLineBreaks - if false don't use AppendLine() - StringBuilder finalSql = new StringBuilder(); + if (_cachedCombinedQuery != null) + return _cachedCombinedQuery; - // If Query Template is provided, we assume it contains both SELECT and FROMs - if (_selectColumns.Any()) - finalSql.AppendLine($"SELECT {(_isSelectDistinct ? "DISTINCT " : "")}{string.Join(", ", _selectColumns)}"); + _cachedCombinedQuery = new InterpolatedSqlBuilder(Options); + + _cachedCombinedQuery.AppendLiteral("SELECT ").AppendLiteral(_isSelectDistinct ? "DISTINCT " : ""); + if (_selects.IsEmpty) + _cachedCombinedQuery.AppendLiteral("*"); else - finalSql.AppendLine($"SELECT {(_isSelectDistinct ? "DISTINCT " : "")}*"); + _cachedCombinedQuery.Append(_selects); + + + + if (!_froms.IsEmpty) + { + _froms.TrimEnd(); + _cachedCombinedQuery.AppendLine(_froms); //TODO: inner join and left/outer join shortcuts? + // TODO: AppendLine adds linebreak BEFORE the value - is that a little counterintuitive? + } - if (_fromTables.Any()) - finalSql.AppendLine($"{string.Join(Environment.NewLine, _fromTables)}"); //TODO: inner join and left/outer join shortcuts? - string filters = _queryBuilder.GetFilters(); - if (filters != null) - finalSql.AppendLine("WHERE " + filters); + if (_filters.Any()) + { + var filters = GetFilters()!; - if (_groupBy.Any()) - finalSql.AppendLine($"GROUP BY {string.Join(", ", _groupBy)}"); - if (_having.Any()) - finalSql.AppendLine($"HAVING {string.Join(" AND ", _having)}"); - if (_orderBy.Any()) - finalSql.AppendLine($"ORDER BY {string.Join(", ", _orderBy)}"); + _cachedCombinedQuery.AppendLine().AppendLiteral("WHERE ").Append(filters); + } + + if (!_groupBy.IsEmpty) + _cachedCombinedQuery.AppendLine().AppendLiteral("GROUP BY").Append(_groupBy); + if (!_having.IsEmpty) + _cachedCombinedQuery.AppendLine().AppendLiteral("HAVING ").Append(_having); + if (!_orderBy.IsEmpty) + _cachedCombinedQuery.AppendLine().AppendLiteral("ORDER BY ").Append(_orderBy); if (_rowCount != null) - finalSql.AppendLine($"OFFSET {_offset ?? 0} ROWS FETCH NEXT {_rowCount} ROWS ONLY"); // TODO: PostgreSQL? "LIMIT row_count OFFSET offset" + _cachedCombinedQuery.AppendLine().AppendLiteral("OFFSET ").AppendLiteral((_offset ?? 0).ToString()) + .AppendLiteral($"ROWS FETCH NEXT {_rowCount} ROWS ONLY"); // TODO: PostgreSQL? "LIMIT row_count OFFSET offset" - return finalSql.ToString(); + return _cachedCombinedQuery; } } - #endregion - - /// - /// Parameters of Query - /// - public ParameterInfos Parameters => _queryBuilder.Parameters; +#endregion - /// - /// Underlying connection - /// - public IDbConnection Connection => _queryBuilder.Connection; - #endregion +#endregion } -} +} \ No newline at end of file diff --git a/src/DapperQueryBuilder/FluentQueryBuilder/IFromBuilder.cs b/src/DapperQueryBuilder/FluentQueryBuilder/IFromBuilder.cs index 942787d..3d8a9ca 100644 --- a/src/DapperQueryBuilder/FluentQueryBuilder/IFromBuilder.cs +++ b/src/DapperQueryBuilder/FluentQueryBuilder/IFromBuilder.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +using InterpolatedSql; +using System; namespace DapperQueryBuilder { diff --git a/src/DapperQueryBuilder/FluentQueryBuilder/ISelectDistinctBuilder.cs b/src/DapperQueryBuilder/FluentQueryBuilder/ISelectDistinctBuilder.cs index b19091f..7a6575b 100644 --- a/src/DapperQueryBuilder/FluentQueryBuilder/ISelectDistinctBuilder.cs +++ b/src/DapperQueryBuilder/FluentQueryBuilder/ISelectDistinctBuilder.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; +using InterpolatedSql; namespace DapperQueryBuilder { diff --git a/src/DapperQueryBuilder/FluentQueryBuilder/IWhereBuilder.cs b/src/DapperQueryBuilder/FluentQueryBuilder/IWhereBuilder.cs index 11f2328..a3d9e59 100644 --- a/src/DapperQueryBuilder/FluentQueryBuilder/IWhereBuilder.cs +++ b/src/DapperQueryBuilder/FluentQueryBuilder/IWhereBuilder.cs @@ -1,4 +1,5 @@ -using System; +using InterpolatedSql; +using System; using System.Collections.Generic; using System.Text; diff --git a/src/DapperQueryBuilder/ICommand.cs b/src/DapperQueryBuilder/ICommand.cs index 596d287..1603626 100644 --- a/src/DapperQueryBuilder/ICommand.cs +++ b/src/DapperQueryBuilder/ICommand.cs @@ -1,8 +1,5 @@ -using Dapper; -using System; -using System.Collections.Generic; +using System; using System.Data; -using System.Text; namespace DapperQueryBuilder { @@ -19,11 +16,13 @@ public interface ICommand /// /// Parameters of Command /// - ParameterInfos Parameters { get; } + ParametersDictionary DapperParameters { get; } + + [Obsolete("Use DapperParameters")] ParametersDictionary Parameters { get; } /// /// Underlying connection /// - IDbConnection Connection { get; } + IDbConnection DbConnection { get; } } } diff --git a/src/DapperQueryBuilder/ICompleteCommand.cs b/src/DapperQueryBuilder/ICompleteCommand.cs index ec49fd5..b5934b1 100644 --- a/src/DapperQueryBuilder/ICompleteCommand.cs +++ b/src/DapperQueryBuilder/ICompleteCommand.cs @@ -1,15 +1,11 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Data; -using System.Text; +using InterpolatedSql; namespace DapperQueryBuilder { /// /// Any command (Contains Connection, SQL, and Parameters) which is complete for execution. /// - public interface ICompleteCommand : ICommand + public interface ICompleteCommand : ICommand, IInterpolatedSql { } } diff --git a/src/DapperQueryBuilder/ICompleteCommandExtensions.cs b/src/DapperQueryBuilder/ICompleteCommandExtensions.cs index 01b9ea8..15f240d 100644 --- a/src/DapperQueryBuilder/ICompleteCommandExtensions.cs +++ b/src/DapperQueryBuilder/ICompleteCommandExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Text; using System.Threading.Tasks; using Dapper; @@ -19,7 +18,7 @@ public static class ICompleteCommandExtensions /// public static int Execute(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.Execute(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.Execute(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -27,7 +26,7 @@ public static int Execute(this ICompleteCommand command, IDbTransaction transact /// public static Task ExecuteAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -38,7 +37,7 @@ public static Task ExecuteAsync(this ICompleteCommand command, IDbTransacti /// public static object ExecuteScalar(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteScalar(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteScalar(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -46,7 +45,7 @@ public static object ExecuteScalar(this ICompleteCommand command, IDbTransaction /// public static Task ExecuteScalarAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteScalarAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteScalarAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -54,7 +53,7 @@ public static Task ExecuteScalarAsync(this ICompleteCommand command, IDbTr /// public static Task ExecuteScalarAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteScalarAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteScalarAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -65,7 +64,8 @@ public static Task ExecuteScalarAsync(this ICompleteCommand command, IDb /// public static SqlMapper.GridReader QueryMultiple(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryMultiple(sql: command.Sql, param: command.Parameters.DapperParameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + // DapperParameters because QueryMultiple with Stored Procedures doesn't work with Dictionary - see https://github.com/DapperLib/Dapper/issues/1580#issuecomment-889813797 + return command.DbConnection.QueryMultiple(sql: command.Sql, param: ParametersDictionary.LoadFrom(command).DynamicParameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -73,7 +73,8 @@ public static SqlMapper.GridReader QueryMultiple(this ICompleteCommand command, /// public static Task QueryMultipleAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryMultipleAsync(sql: command.Sql, param: command.Parameters.DapperParameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + // DapperParameters because QueryMultiple with Stored Procedures doesn't work with Dictionary - see https://github.com/DapperLib/Dapper/issues/1580#issuecomment-889813797 + return command.DbConnection.QueryMultipleAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command).DynamicParameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -84,7 +85,7 @@ public static SqlMapper.GridReader QueryMultiple(this ICompleteCommand command, /// public static IEnumerable Query(this ICompleteCommand command, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.Query(sql: command.Sql, param: command.Parameters, transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.Query(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -92,7 +93,7 @@ public static IEnumerable Query(this ICompleteCommand command, IDbTransact /// public static T QueryFirst(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirst(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirst(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -100,7 +101,7 @@ public static T QueryFirst(this ICompleteCommand command, IDbTransaction tran /// public static T QueryFirstOrDefault(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefault(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefault(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -108,7 +109,7 @@ public static T QueryFirstOrDefault(this ICompleteCommand command, IDbTransac /// public static T QuerySingle(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingle(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingle(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -116,7 +117,7 @@ public static T QuerySingle(this ICompleteCommand command, IDbTransaction tra /// public static T QuerySingleOrDefault(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefault(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefault(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -126,7 +127,7 @@ public static T QuerySingleOrDefault(this ICompleteCommand command, IDbTransa /// public static IEnumerable Query(this ICompleteCommand command, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.Query(sql: command.Sql, param: command.Parameters, transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.Query(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -134,7 +135,7 @@ public static IEnumerable Query(this ICompleteCommand command, IDbTrans /// public static dynamic QueryFirst(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirst(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirst(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -142,7 +143,7 @@ public static dynamic QueryFirst(this ICompleteCommand command, IDbTransaction t /// public static dynamic QueryFirstOrDefault(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefault(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefault(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -150,7 +151,7 @@ public static dynamic QueryFirstOrDefault(this ICompleteCommand command, IDbTran /// public static dynamic QuerySingle(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingle(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingle(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -158,7 +159,7 @@ public static dynamic QuerySingle(this ICompleteCommand command, IDbTransaction /// public static dynamic QuerySingleOrDefault(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefault(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefault(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -168,7 +169,7 @@ public static dynamic QuerySingleOrDefault(this ICompleteCommand command, IDbTra /// public static IEnumerable Query(this ICompleteCommand command, Type type, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.Query(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.Query(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, buffered: buffered, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -176,7 +177,7 @@ public static IEnumerable Query(this ICompleteCommand command, Type type /// public static object QueryFirst(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirst(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirst(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -184,7 +185,7 @@ public static object QueryFirst(this ICompleteCommand command, Type type, IDbTra /// public static object QueryFirstOrDefault(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefault(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefault(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -192,7 +193,7 @@ public static object QueryFirstOrDefault(this ICompleteCommand command, Type typ /// public static object QuerySingle(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingle(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingle(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -200,7 +201,7 @@ public static object QuerySingle(this ICompleteCommand command, Type type, IDbTr /// public static object QuerySingleOrDefault(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefault(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefault(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -210,7 +211,7 @@ public static object QuerySingleOrDefault(this ICompleteCommand command, Type ty /// public static Task> QueryAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -218,28 +219,28 @@ public static Task> QueryAsync(this ICompleteCommand command, /// public static Task QueryFirstAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as T. /// public static Task QueryFirstOrDefaultAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefaultAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefaultAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as T. /// public static Task QuerySingleAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as T. /// public static Task QuerySingleOrDefaultAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefaultAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefaultAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -249,7 +250,7 @@ public static Task QuerySingleOrDefaultAsync(this ICompleteCommand command /// public static Task> QueryAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -257,7 +258,7 @@ public static Task> QueryAsync(this ICompleteCommand comman /// public static Task QueryFirstAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -265,7 +266,7 @@ public static Task QueryFirstAsync(this ICompleteCommand command, IDbTr /// public static Task QueryFirstOrDefaultAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefaultAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefaultAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -273,7 +274,7 @@ public static Task QueryFirstOrDefaultAsync(this ICompleteCommand comma /// public static Task QuerySingleAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -281,7 +282,7 @@ public static Task QuerySingleAsync(this ICompleteCommand command, IDbT /// public static Task QuerySingleOrDefaultAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefaultAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefaultAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -291,7 +292,7 @@ public static Task QuerySingleOrDefaultAsync(this ICompleteCommand comm /// public static Task> QueryAsync(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryAsync(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryAsync(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -299,28 +300,28 @@ public static Task> QueryAsync(this ICompleteCommand command /// public static Task QueryFirstAsync(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstAsync(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstAsync(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as type. /// public static Task QueryFirstOrDefaultAsync(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QueryFirstOrDefaultAsync(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QueryFirstOrDefaultAsync(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as type. /// public static Task QuerySingleAsync(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleAsync(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleAsync(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// /// Executes the query (using Dapper), returning the data typed as type. /// public static Task QuerySingleOrDefaultAsync(this ICompleteCommand command, Type type, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.QuerySingleOrDefaultAsync(type: type, sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.QuerySingleOrDefaultAsync(type: type, sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion @@ -330,7 +331,7 @@ public static Task QuerySingleOrDefaultAsync(this ICompleteCommand comma /// public static IDataReader ExecuteReader(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteReader(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteReader(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } /// @@ -338,9 +339,10 @@ public static IDataReader ExecuteReader(this ICompleteCommand command, IDbTransa /// public static Task ExecuteReaderAsync(this ICompleteCommand command, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - return command.Connection.ExecuteReaderAsync(sql: command.Sql, param: command.Parameters, transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); + return command.DbConnection.ExecuteReaderAsync(sql: command.Sql, param: ParametersDictionary.LoadFrom(command), transaction: transaction, commandTimeout: commandTimeout, commandType: commandType); } #endregion + } } diff --git a/src/DapperQueryBuilder/IDbConnectionExtensions.cs b/src/DapperQueryBuilder/IDbConnectionExtensions.cs index 0e81013..3d67db8 100644 --- a/src/DapperQueryBuilder/IDbConnectionExtensions.cs +++ b/src/DapperQueryBuilder/IDbConnectionExtensions.cs @@ -1,15 +1,18 @@ -using System; -using System.Collections.Generic; +using InterpolatedSql; +using System; using System.Data; -using System.Text; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif namespace DapperQueryBuilder { /// /// Extends IDbConnection to easily build QueryBuilder or FluentQueryBuilder /// - public static class IDbConnectionExtensions + public static class IDbConnectionExtensions //TODO: all factories here could be delegated to a Factory class, so that we can replace the factory { + #region Fluent Query Builder /// /// Creates a new empty FluentQueryBuilder over current connection /// @@ -18,8 +21,10 @@ public static IEmptyQueryBuilder FluentQueryBuilder(this IDbConnection cnn) { return new FluentQueryBuilder(cnn); } + #endregion + #region QueryBuilder /// /// Creates a new QueryBuilder over current connection /// @@ -40,24 +45,101 @@ public static QueryBuilder QueryBuilder(this IDbConnection cnn) { return new QueryBuilder(cnn); } + #endregion + #region SqlBuilder +#if NET6_0_OR_GREATER /// - /// Creates a new CommandBuilder over current connection + /// Creates a new SqlBuilder over current connection /// /// /// SQL command - public static CommandBuilder CommandBuilder(this IDbConnection cnn, FormattableString command) + public static SqlBuilder SqlBuilder(this IDbConnection cnn, ref InterpolatedSqlHandler value) { - return new CommandBuilder(cnn, command); + if (value.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) + value.AdjustMultilineString(); + return new SqlBuilder(cnn, value.InterpolatedSqlBuilder); } + /// + /// Creates a new SqlBuilder over current connection + /// + /// + /// SQL command + public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, [InterpolatedStringHandlerArgument("options")] ref InterpolatedSqlHandler value) + { + if (value.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) + value.AdjustMultilineString(); + return new SqlBuilder(cnn, value.InterpolatedSqlBuilder); + } + +#else + /// + /// Creates a new SqlBuilder over current connection + /// + /// + /// SQL command + public static SqlBuilder SqlBuilder(this IDbConnection cnn, FormattableString command) + { + return new SqlBuilder(cnn, command); + } + /// + /// Creates a new SqlBuilder over current connection + /// + /// + /// SQL command + public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, FormattableString command) + { + return new SqlBuilder(cnn, command, options); + } +#endif /// - /// Creates a new empty CommandBuilder over current connection + /// Creates a new empty SqlBuilder over current connection /// - public static CommandBuilder CommandBuilder(this IDbConnection cnn) + public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions? options = null) + { + return new SqlBuilder(cnn, options); + } + #endregion + + #region SqlBuilder (backwards compatibility - legacy extension named CommandBuilder()) +#if NET6_0_OR_GREATER + [Obsolete("Please use new extension SqlBuilder()")] + public static SqlBuilder CommandBuilder(this IDbConnection cnn, ref InterpolatedSqlHandler value) + { + if (value.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) + value.AdjustMultilineString(); + return new SqlBuilder(cnn, value.InterpolatedSqlBuilder); + } + + [Obsolete("Please use new extension SqlBuilder()")] + public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, [InterpolatedStringHandlerArgument("options")] ref InterpolatedSqlHandler value) + { + if (value.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) + value.AdjustMultilineString(); + return new SqlBuilder(cnn, value.InterpolatedSqlBuilder); + } + +#else + [Obsolete("Please use new extension SqlBuilder()")] + public static SqlBuilder CommandBuilder(this IDbConnection cnn, FormattableString command) + { + return new SqlBuilder(cnn, command); + } + + [Obsolete("Please use new extension SqlBuilder()")] + public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, FormattableString command) + { + return new SqlBuilder(cnn, command, options); + } +#endif + + [Obsolete("Please use new extension SqlBuilder()")] + public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions? options = null) { - return new CommandBuilder(cnn); + return new SqlBuilder(cnn, options); } + #endregion } } diff --git a/src/DapperQueryBuilder/IFilter.cs b/src/DapperQueryBuilder/IFilter.cs deleted file mode 100644 index a602b34..0000000 --- a/src/DapperQueryBuilder/IFilter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Text; - -namespace DapperQueryBuilder -{ - /// - /// Can be both individual filter or a list of filters. - /// - public interface IFilter - { - /// - /// Writes the SQL Statement of the filter - /// - void WriteFilter(StringBuilder sb); - - /// - /// Merges parameters from this filter into a CommandBuilder.
- /// Checks for name clashes, and will rename parameters (in CommandBuilder) if necessary.
- /// If some parameter is renamed the underlying Sql statement will have the new parameter names replaced by their new names.
- /// This method does NOT append Parser SQL to CommandBuilder SQL (you may want to save this SQL statement elsewhere) - ///
- void MergeParameters(ParameterInfos target); - } -} diff --git a/src/DapperQueryBuilder/InterpolatedStatementParser.cs b/src/DapperQueryBuilder/InterpolatedStatementParser.cs deleted file mode 100644 index 3e279f9..0000000 --- a/src/DapperQueryBuilder/InterpolatedStatementParser.cs +++ /dev/null @@ -1,301 +0,0 @@ -using Dapper; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace DapperQueryBuilder -{ - /// - /// Parses an interpolated-string SQL statement into a injection-safe statement (with parameters as @p0, @p1, etc) and a dictionary of parameter values. - /// - [DebuggerDisplay("{Sql} ({_parametersStr,nq})")] - public class InterpolatedStatementParser - { - #region Members - /// - /// Injection-safe statement, with parameters as @p0, @p1, etc. - /// - public string Sql { get; set; } - - /// - /// Dictionary of Dapper parameters - /// - public ParameterInfos Parameters { get; set; } - - - private string _parametersStr; - - private static Regex _formattableArgumentRegex = new Regex( - "{(?\\d*)(:(?[^}]*))?}", - RegexOptions.IgnoreCase - | RegexOptions.Singleline - | RegexOptions.CultureInvariant - | RegexOptions.IgnorePatternWhitespace - | RegexOptions.Compiled - ); - - private static Regex quotedVariableStart = new Regex("(^|\\s+|=|>|<|>=|<=|<>)'$", - RegexOptions.IgnoreCase - | RegexOptions.Singleline - | RegexOptions.CultureInvariant - | RegexOptions.IgnorePatternWhitespace - | RegexOptions.Compiled - ); - private static Regex quotedVariableEnd = new Regex("^'($|\\s+|=|>|<|>=|<=|<>)", - RegexOptions.IgnoreCase - | RegexOptions.Singleline - | RegexOptions.CultureInvariant - | RegexOptions.IgnorePatternWhitespace - | RegexOptions.Compiled - ); - #endregion - - #region Regex - // String(maxlength) / nvarchar(maxlength) / String / nvarchar - private static Regex regexDbTypeString = new Regex("^(String|nvarchar)\\s*(\\(\\s*(?\\d*)\\s*\\))?$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - - // StringFixedLength(length) / nchar(length) / StringFixedLength / nchar - private static Regex regexDbTypeStringFixedLength = new Regex("^(StringFixedLength|nchar)\\s*(\\(\\s*(?\\d*)\\s*\\))?$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - - // AnsiString(maxlength) / varchar(maxlength) / AnsiString / varchar - private static Regex regexDbTypeAnsiString = new Regex("^(AnsiString|varchar)\\s*(\\(\\s*(?\\d*)\\s*\\))?$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - - // AnsiStringFixedLength(length) / char(length) / AnsiStringFixedLength / char - private static Regex regexDbTypeAnsiStringFixedLength = new Regex("^(AnsiStringFixedLength|char)\\s*(\\(\\s*(?\\d*)\\s*\\))?$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - - // text / varchar(MAX) / varchar(-1) - private static Regex regexDbTypeText = new Regex("^(text|varchar\\s*(\\(\\s*((MAX|-1))\\s*\\)))$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - - // ntext / nvarchar(MAX) / nvarchar(-1) - private static Regex regexDbTypeNText = new Regex("^(ntext|nvarchar\\s*(\\(\\s*((MAX|-1))\\s*\\)))$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled); - #endregion - - #region ctor - /// - /// Parses an interpolated-string SQL statement into a injection-safe statement (with parameters as @p0, @p1, etc) and a dictionary of parameter values. - /// - /// - public InterpolatedStatementParser(FormattableString query) : this(query.Format, query.GetArguments()) - { - } - private InterpolatedStatementParser(string format, params object[] arguments) - { - Parameters = new ParameterInfos(); - - StringBuilder sb = new StringBuilder(); - if (string.IsNullOrEmpty(format)) - return; - var matches = _formattableArgumentRegex.Matches(format); - int lastPos = 0; - for (int i = 0; i < matches.Count; i++) - { - // unescape escaped curly braces - string sql = format.Substring(lastPos, matches[i].Index - lastPos).Replace("{{", "{").Replace("}}", "}"); - lastPos = matches[i].Index + matches[i].Length; - - // arguments[i] may not work because same argument can be used multiple times - int argPos = int.Parse(matches[i].Groups["ArgPos"].Value); - string argFormat = matches[i].Groups["Format"].Value; - List argFormats = argFormat.Split(new char[] { ',', '|' }, StringSplitOptions.RemoveEmptyEntries).Select(f => f.Trim()).ToList(); - object arg = arguments[argPos]; - - if (argFormats.Contains("raw")) // example: {nameof(Product.Name):raw} -> won't be parametrized, we just emit raw string! - { - sb.Append(sql); - sb.Append(arg); - continue; - } - else if (arg is FormattableString fsArg) //Support nested FormattableString - { - sb.Append(sql); - var nestedStatement = new InterpolatedStatementParser(fsArg); - string subSql = nestedStatement.Sql; - if (nestedStatement.Parameters.Any()) - { - subSql = (Parameters.MergeParameters(nestedStatement.Parameters, nestedStatement.Sql) ?? subSql); - } - sb.Append(subSql); - continue; - } - else if (arg is ICompleteCommand innerQuery) //Support nested QueryBuilder, CommandBuilder or FluentQueryBuilder - { - sb.Append(sql); - string innerSql = innerQuery.Sql; - // Convert back from @p0 @p1 etc to {0} {1} etc (FormattableString standard) //TODO: this is really ugly, we should store query internally using a better data structure - Regex matchParametersRegex = new Regex("(?:[,~=<>*/%+&|^-]|\\s|\\b|^)* " + "(" + DapperQueryBuilderOptions.DatabaseParameterSymbol + DapperQueryBuilderOptions.AutoGeneratedParameterName + $"(?:{DapperQueryBuilderOptions.ParameterArrayNameSuffix}){{0,1}}"+ "(?\\d*)" + ")" + " (?:[,~=<>*/%+&|^-]|\\s|\\b|$)", - RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - innerSql = matchParametersRegex.Replace(innerSql, match => { - Group parm = match.Groups[1]; - int parmNum = int.Parse(match.Groups[2].Value); - string replace = "{" + parmNum + "}"; - string ret = string.Format("{0}{1}{2}", match.Value.Substring(0, parm.Index - match.Index), replace, match.Value.Substring(parm.Index - match.Index + parm.Length)); - return ret; - }); - - - var nestedStatement = new InterpolatedStatementParser(innerSql, innerQuery.Parameters.Select(p => p.Value.Value).ToArray()); - string subSql = nestedStatement.Sql; - if (nestedStatement.Parameters.Any()) - { - subSql = (Parameters.MergeParameters(nestedStatement.Parameters, nestedStatement.Sql) ?? subSql); - } - sb.Append(subSql); - continue; - } - // If user passes " column LIKE '{variable}' ", we assume that he used single quotes incorrectly as if interpolated string was a sql literal - if (quotedVariableStart.IsMatch(sql) && quotedVariableEnd.IsMatch(format.Substring(lastPos))) - { - sql = sql.Substring(0, sql.Length - 1); // skip starting quote - lastPos++; // skip closing quote - } - - sb.Append(sql); - - var direction = System.Data.ParameterDirection.Input; - System.Data.DbType? dbType = null; - if (argFormats.Contains("out")) - direction = System.Data.ParameterDirection.Output; - - System.Data.DbType parsedDbType; - Match m; - foreach (var f in argFormats) - { - if (arg is string && (m = regexDbTypeString.Match(f)) != null && m.Success) // String(maxlength) / nvarchar(maxlength) / String / nvarchar - arg = new DbString() - { - IsAnsi = false, - IsFixedLength = false, - Value = (string)arg, - Length = (string.IsNullOrEmpty(m.Groups["maxlength"].Value) ? Math.Max(DbString.DefaultLength, ((string)arg).Length) : int.Parse(m.Groups["maxlength"].Value)) - }; - else if (arg is string && (m = regexDbTypeAnsiString.Match(f)) != null && m.Success) // AnsiString(maxlength) / varchar(maxlength) / AnsiString / varchar - arg = new DbString() - { - IsAnsi = true, - IsFixedLength = false, - Value = (string)arg, - Length = (string.IsNullOrEmpty(m.Groups["maxlength"].Value) ? Math.Max(DbString.DefaultLength, ((string)arg).Length) : int.Parse(m.Groups["maxlength"].Value)) - }; - else if (arg is string && (m = regexDbTypeStringFixedLength.Match(f)) != null && m.Success) // StringFixedLength(length) / nchar(length) / StringFixedLength / nchar - arg = new DbString() - { - IsAnsi = false, - IsFixedLength = true, - Value = (string)arg, - Length = (string.IsNullOrEmpty(m.Groups["length"].Value) ? ((string)arg).Length : int.Parse(m.Groups["length"].Value)) - }; - else if (arg is string && (m = regexDbTypeAnsiStringFixedLength.Match(f)) != null && m.Success) // AnsiStringFixedLength(length) / char(length) / AnsiStringFixedLength / char - arg = new DbString() - { - IsAnsi = true, - IsFixedLength = true, - Value = (string)arg, - Length = (string.IsNullOrEmpty(m.Groups["length"].Value) ? ((string)arg).Length : int.Parse(m.Groups["length"].Value)) - }; - else if (arg is string && (m = regexDbTypeText.Match(f)) != null && m.Success) // text / varchar(MAX) / varchar(-1) - arg = new DbString() - { - IsAnsi = false, - IsFixedLength = true, - Value = (string)arg, - Length = int.MaxValue - }; - else if (arg is string && (m = regexDbTypeNText.Match(f)) != null && m.Success) // ntext / nvarchar(MAX) / nvarchar(-1) - arg = new DbString() - { - IsAnsi = true, - IsFixedLength = true, - Value = (string)arg, - Length = int.MaxValue - }; - - else if (arg is IEnumerable && (m = regexDbTypeString.Match(f)) != null && m.Success) // String(maxlength) / nvarchar(maxlength) / String / nvarchar - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = false, - IsFixedLength = false, - Value = str, - Length = (string.IsNullOrEmpty(m.Groups["maxlength"].Value) ? Math.Max(DbString.DefaultLength, ((string)arg).Length) : int.Parse(m.Groups["maxlength"].Value)) - }); - else if (arg is IEnumerable && (m = regexDbTypeAnsiString.Match(f)) != null && m.Success) // AnsiString(maxlength) / varchar(maxlength) / AnsiString / varchar - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = true, - IsFixedLength = false, - Value = str, - Length = (string.IsNullOrEmpty(m.Groups["maxlength"].Value) ? Math.Max(DbString.DefaultLength, ((string)arg).Length) : int.Parse(m.Groups["maxlength"].Value)) - }); - else if (arg is IEnumerable && (m = regexDbTypeStringFixedLength.Match(f)) != null && m.Success) // StringFixedLength(length) / nchar(length) / StringFixedLength / nchar - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = false, - IsFixedLength = true, - Value = str, - Length = (string.IsNullOrEmpty(m.Groups["length"].Value) ? ((string)arg).Length : int.Parse(m.Groups["length"].Value)) - }); - else if (arg is IEnumerable && (m = regexDbTypeAnsiStringFixedLength.Match(f)) != null && m.Success) // AnsiStringFixedLength(length) / char(length) / AnsiStringFixedLength / char - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = true, - IsFixedLength = true, - Value = str, - Length = (string.IsNullOrEmpty(m.Groups["length"].Value) ? ((string)arg).Length : int.Parse(m.Groups["length"].Value)) - }); - else if (arg is IEnumerable && (m = regexDbTypeText.Match(f)) != null && m.Success) // text / varchar(MAX) / varchar(-1) - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = false, - IsFixedLength = true, - Value = str, - Length = int.MaxValue - }); - else if (arg is IEnumerable && (m = regexDbTypeNText.Match(f)) != null && m.Success) // ntext / nvarchar(MAX) / nvarchar(-1) - arg = ((IEnumerable)arg).Select(str => new DbString() - { - IsAnsi = true, - IsFixedLength = true, - Value = str, - Length = int.MaxValue - }); - - else if (!(arg is DbString) && dbType == null && Enum.TryParse(value: f, ignoreCase: true, result: out parsedDbType)) - { - dbType = parsedDbType; - } - - //TODO: parse SqlDbTypes? - // https://stackoverflow.com/questions/35745226/net-system-type-to-sqldbtype - // https://gist.github.com/tecmaverick/858392/53ddaaa6418b943fa3a230eac49a9efe05c2d0ba - } - sb.Append(DapperQueryBuilderOptions.DatabaseParameterSymbol + Parameters.Add( arg, dbType, direction ) ); - } - string lastPart = format.Substring(lastPos).Replace("{{", "{").Replace("}}", "}"); - sb.Append(lastPart); - Sql = sb.ToString(); - _parametersStr = string.Join(", ", Parameters.ParameterNames.ToList().Select(n => DapperQueryBuilderOptions.DatabaseParameterSymbol + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - } - #endregion - - /// - /// Merges parameters from this query/statement into a CommandBuilder.
- /// Checks for name clashes, and will rename parameters (in CommandBuilder) if necessary.
- /// If some parameter is renamed the underlying Sql statement will have the new parameter names replaced by their new names.
- /// This method does NOT append Parser SQL to CommandBuilder SQL (you may want to save this SQL statement elsewhere) - ///
- public void MergeParameters(ParameterInfos target) - { - string newSql = target.MergeParameters(Parameters, Sql); - if (newSql != null) - { - Sql = newSql; - _parametersStr = string.Join(", ", Parameters.ParameterNames.ToList().Select(n => DapperQueryBuilderOptions.DatabaseParameterSymbol + n + "='" + Convert.ToString(Parameters.Get(n)) + "'")); - // filter parameters in Sql were renamed and won't match the previous passed filters - discard original parameters to avoid reusing wrong values - Parameters = null; - } - } - - } -} diff --git a/src/DapperQueryBuilder/ParameterInfo.cs b/src/DapperQueryBuilder/ParameterInfo.cs deleted file mode 100644 index eee4277..0000000 --- a/src/DapperQueryBuilder/ParameterInfo.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace DapperQueryBuilder -{ - /// - /// SQL parameter which is passed to Dapper - /// - [System.Diagnostics.DebuggerDisplay("{Name,nq} = {Value,nq}")] - public class ParameterInfo - { - #region Members - /// - /// Auto-generated name of parameter like p0, p1, etc. Does NOT contain database-specific prefixes like @ or : - /// - public string Name { get; set; } - - /// - /// Value of parameter - /// - public object Value { get; set; } - - /// - /// Parameters added through string interpolation are usually input parameters (passed from C# to SQL),
- /// but you may explicitly describe parameters as Output, InputOutput, or ReturnValues. - ///
- public ParameterDirection ParameterDirection { get; set; } - - /// - /// Parameters added through string interpolation usually do not need to define their DbType, and Dapper will automatically detect the correct type,
- /// but it's possible to explicitly define the DbType (which Dapper will map to corresponding type in your database) - ///
- public DbType? DbType { get; set; } - - /// - /// Parameters added through string interpolation usually do not need to define their Size, and Dapper will automatically detect the correct size,
- /// but it's possible to explicitly define the size (usually for strings, where in some specific scenarios you can get better performance by passing the exact data type) - ///
- public int? Size { get; set; } - - /// - /// Parameters added through string interpolation usually do not need to define this, as Dapper will automatically calculate the correct value - /// - public byte? Precision { get; set; } - - /// - /// Parameters added through string interpolation usually do not need to define this, as Dapper will automatically calculate the correct value - /// - public byte? Scale { get; set; } - - internal Action OutputCallback { get; set; } - #endregion - - #region ctors - /// - /// New Parameter - /// - /// The name of the parameter. - /// The value of the parameter. - /// The type of the parameter. - /// The in or out direction of the parameter. - /// The size of the parameter. - public ParameterInfo(string name, object value, DbType? dbType, ParameterDirection? direction, int? size) : this(name, value, dbType, direction, size, null, null) - { - } - - /// - /// New Parameter - /// - /// The name of the parameter. - /// The value of the parameter. - /// The type of the parameter. - /// The in or out direction of the parameter. - /// The size of the parameter. - /// The precision of the parameter. - /// The scale of the parameter. - public ParameterInfo(string name, object value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null) - { - this.Name = name; - this.Value = value; - this.DbType = dbType; - this.ParameterDirection = direction ?? ParameterDirection.Input; - this.Size = size; - this.Precision = precision; - this.Scale = scale; - } - - /// - /// Creates a new Output Parameter (can be Output, InputOutput, or ReturnValue)
- /// and registers a callback action which (after command invocation) will populate back parameter output value into an instance property. - ///
- /// The name of the parameter. - /// Target variable where output value will be set. - /// Property where output value will be set. If it's InputOutput type this value will be passed. - /// The type of the parameter. - /// The type of output of the parameter. - /// The size of the parameter. - /// The precision of the parameter. - /// The scale of the parameter. - public static ParameterInfo CreateOutputParameter(string name, T target, Expression> expression, OutputParameterDirection direction = OutputParameterDirection.Output, DbType? dbType = null, int? size = null, byte? precision = null, byte? scale = null) - { - object value = null; - - // For InputOutput we send current value - if (direction == OutputParameterDirection.InputOutput) - value = expression.Compile().Invoke(target); - - ParameterInfo parameter = new ParameterInfo(name, value, dbType, (ParameterDirection)direction, size, precision, scale); - - var setter = GetSetter(expression); - parameter.OutputCallback = new Action(o => - { - TP val; - if (o is TP) - val = (TP)o; - else - { - try - { - val = (TP)Convert.ChangeType(o, typeof(TP)); - } - catch (Exception ex) - { - throw new Exception($"Can't convert {parameter.Name} ({parameter.Value}) to type {typeof(TP).Name}", ex); - } - } - setter(target, val); // TP (property type) must match the return value - }); - - return parameter; - } - #endregion - - #region Enums - /// - /// Type of Output - /// - public enum OutputParameterDirection - { - /// - /// The parameter is an output parameter. - /// - Output = 2, - - /// - /// The parameter is capable of both input and output. - /// - InputOutput = 3, - - /// - /// The parameter represents a return value from an operation such as a stored procedure, built-in function, or user-defined function. - /// - ReturnValue = 6 - } - #endregion - - /// - /// Convert a lambda expression for a getter into a setter - /// - private static Action GetSetter(Expression> expression) - { - var memberExpression = (MemberExpression)expression.Body; - var property = (PropertyInfo)memberExpression.Member; - var setMethod = property.GetSetMethod(); - - var parameterT = Expression.Parameter(typeof(T), "x"); - var parameterTProperty = Expression.Parameter(typeof(TProperty), "y"); - - var newExpression = - Expression.Lambda>( - Expression.Call(parameterT, setMethod, parameterTProperty), - parameterT, - parameterTProperty - ); - - return newExpression.Compile(); - } - - } -} diff --git a/src/DapperQueryBuilder/ParameterInfos.cs b/src/DapperQueryBuilder/ParameterInfos.cs deleted file mode 100644 index 361d626..0000000 --- a/src/DapperQueryBuilder/ParameterInfos.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Dapper; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; -using System.Text.RegularExpressions; - -namespace DapperQueryBuilder -{ - /// - /// A List of Parameters that are passed to Dapper methods - /// - public class ParameterInfos : Dictionary, SqlMapper.IDynamicParameters, SqlMapper.IParameterCallbacks - { - #region members - private DynamicParameters _dapperParameters = null; - #endregion - - #region ctors - /// - /// List of SQL parameters which are passed to Dapper - /// - public ParameterInfos() : base(StringComparer.OrdinalIgnoreCase) - { - } - #endregion - - #region DapperParameters - /// - /// Convert the current parameters into Dapper Parameters, since Dapper will automagically set DbTypes, Sizes, etc, and map to our databases - /// - public virtual DynamicParameters DapperParameters - { - get - { - if (_dapperParameters == null) - { - _dapperParameters = new DynamicParameters(); - foreach (var parameter in this.Values) - { - _dapperParameters.Add(parameter.Name, parameter.Value, parameter.DbType, parameter.ParameterDirection, parameter.Size); - } - } - return _dapperParameters; - } - } - #endregion - - /// - /// Add a parameter to this dynamic parameter list. - /// - public void Add(ParameterInfo parameter) - { - this[parameter.Name] = parameter; - } - - bool IsEnumerable(object value) - { - if (value == null || value is DBNull) //SqlMapper.GetDbType - return false; - Type t = value.GetType(); - return t != typeof(string) && typeof(IEnumerable).IsAssignableFrom(t); - //TODO: use Dapper SqlMapper.LookupDbType ? - } - - /// - /// Add a parameter to this dynamic parameter list (reusing existing parameter if possible) - /// - public string Add(object value, DbType? dbType = null, ParameterDirection? direction = null) - { - bool isEnumerable = IsEnumerable(value); - string newParameterName = DapperQueryBuilderOptions.AutoGeneratedParameterName + (isEnumerable ? DapperQueryBuilderOptions.ParameterArrayNameSuffix : "") + Values.Count; - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) - { - var existingParam = - Values.FirstOrDefault(p => - p.DbType == dbType - && p.ParameterDirection == direction - && direction == ParameterDirection.Input - && ((p.Value == null && value == null) || (p.Value != null && p.Value.Equals(value))) - ); - - if (existingParam != null) - return existingParam.Name; - } - - var parameter = new ParameterInfo(newParameterName, value, dbType, direction); - - this[parameter.Name] = parameter; - return newParameterName; - } - - /// - /// Get parameter value - /// - public T Get(string key) => (T)this[key].Value; - - /// - /// Parameter Names - /// - public HashSet ParameterNames => new HashSet(this.Keys); - - void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) - { - // we just rely on Dapper.DynamicParameters which implements IDynamicParameters like a charm - ((SqlMapper.IDynamicParameters)DapperParameters).AddParameters(command, identity); - } - - /// - /// After Dapper command is executed, we should get output/return parameters back - /// - void SqlMapper.IParameterCallbacks.OnCompleted() - { - var dapperParameters = DapperParameters; - - // Update output and return parameters back - foreach (var oparm in this.Values.Where(p => p.ParameterDirection != ParameterDirection.Input)) - { - oparm.Value = dapperParameters.Get(oparm.Name); - oparm.OutputCallback?.Invoke(oparm.Value); - } - } - - - #region Add Existing Parameter - /// - /// Merges single parameter into this list.
- /// Checks for name clashes, and will rename parameter if necessary.
- /// Will return the name of the merged parameter. - /// (most likely each merged parameter will get a higher auto-generated number, but it's possible that an identical type/value exists and in this case will return the existing parameter name) - ///
- public string MergeParameter(ParameterInfo parameter) - { - if (DapperQueryBuilderOptions.ReuseIdenticalParameters) - { - var existingParam = - Values.FirstOrDefault(p => - p.DbType == parameter.DbType - && p.ParameterDirection == parameter.ParameterDirection - && parameter.ParameterDirection == ParameterDirection.Input - && ((p.Value == null && parameter.Value == null) || (p.Value != null && p.Value.Equals(parameter.Value))) - ); - - // if. we're adding a new block with @p0="Rick" but we already have @p1="Rick" in the existing statement, - // just return "@p1" so that the "@p0" occurences in the appended block will be renamed to @p1 - if (existingParam != null) - return existingParam.Name; - } - - bool isEnumerable = IsEnumerable(parameter.Value); - string newParameterName = DapperQueryBuilderOptions.AutoGeneratedParameterName + (isEnumerable ? DapperQueryBuilderOptions.ParameterArrayNameSuffix : "") + ParameterNames.Count; - - // Create a copy, it's safer - ParameterInfo newParameter = new ParameterInfo( - name: newParameterName, - value: parameter.Value, - dbType: parameter.DbType, - direction: parameter.ParameterDirection, - size: parameter.Size, - precision: parameter.Precision, - scale: parameter.Scale - ); - newParameter.OutputCallback = parameter.OutputCallback; - - Add(newParameter); - - // if. we're adding a new block with @p0="Rick" but we already have other values @p0 and @p1 in the existing statement, - // just return "@p2" (which is the name set in the new parameter) so that the "@p0" occurences in the appended block will be renamed to @p2 - return newParameterName; - } - - /// - /// Merges multiple parameters into this list.
- /// Checks for name clashes, and will rename parameters if necessary.
- /// If some parameter is renamed the returned Sql statement will containg the original sql replaced with new names, else (if nothing changed) returns null.
- ///
- public string MergeParameters(ParameterInfos parameters, string sql) - { - Dictionary renamedParameters = new Dictionary(); - foreach (var parameter in parameters.Values) - { - string newParameterName = MergeParameter(parameter); - if (newParameterName != null && parameter.Name != newParameterName) - renamedParameters.Add(DapperQueryBuilderOptions.DatabaseParameterSymbol + parameter.Name, DapperQueryBuilderOptions.DatabaseParameterSymbol + newParameterName); - } - if (renamedParameters.Any()) - { - Regex matchParametersRegex = new Regex("(?:[,~=<>*/%+&|^-]|\\s|\\b|^)* (" + string.Join("|", renamedParameters.Select(p=>p.Key)) + ") (?:[,~=<>*/%+&|^-]|\\s|\\b|$)", - RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); - string newSql = matchParametersRegex.Replace(sql, match => { - Group group = match.Groups[match.Groups.Count-1]; // last match is the inner parameter - string replace = renamedParameters[group.Value]; - return String.Format("{0}{1}{2}", match.Value.Substring(0, group.Index - match.Index), replace, match.Value.Substring(group.Index - match.Index + group.Length)); - }); - return newSql; - } - return null; - } - - #endregion - - - } - - -} diff --git a/src/DapperQueryBuilder/ParametersDictionary.cs b/src/DapperQueryBuilder/ParametersDictionary.cs new file mode 100644 index 0000000..3b91ab6 --- /dev/null +++ b/src/DapperQueryBuilder/ParametersDictionary.cs @@ -0,0 +1,128 @@ +using Dapper; +using InterpolatedSql; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace DapperQueryBuilder +{ + /// + /// List of SQL Parameters that are passed to Dapper methods + /// + public class ParametersDictionary : Dictionary, SqlMapper.IDynamicParameters, SqlMapper.IParameterCallbacks + { + #region Members + private DynamicParameters? _dynamicParameters = null; + #endregion + + #region ctors + /// + public ParametersDictionary() : base(StringComparer.OrdinalIgnoreCase) + { + } + + /// Creates a built from Implicit Parameters (loaded from ) + /// and Explicit Parameters (loaded from ) + public static ParametersDictionary LoadFrom(IInterpolatedSql sql) + { + var parameters = new ParametersDictionary(); + //HashSet parmNames = new HashSet(StringComparer.OrdinalIgnoreCase); //TODO: check for name clashes, rename as required + + for (int i = 0; i < sql.ExplicitParameters.Count; i++) + { + parameters.Add(sql.ExplicitParameters[i]); + } + + for (int i = 0; i < sql.SqlParameters.Count; i++) + { + // ParseArgument usually just returns parmValue (dbType and direction are only extracted if explicitly defined using format specifiers) + // Dapper will pick the right DbType even if you don't explicitly specify the DbType - and for most cases size don't need to be specified + + var parmName = sql.Options.CalculateAutoParameterName(sql.SqlParameters[i], i); + var parmValue = sql.SqlParameters[i].Argument; + var format = sql.SqlParameters[i].Format; + + if (parmValue is SqlParameterInfo parm) + { + parm.Name = parmName; + parameters[parmName] = parm; + } + else + parameters.Add(new SqlParameterInfo(parmName, parmValue)); + } + return parameters; + } + #endregion + + #region Methods + /// + /// Convert the current parameters into Dapper Parameters, since Dapper will automagically set DbTypes, Sizes, etc, and map to target database + /// + /// + public virtual DynamicParameters DynamicParameters + { + // Most Dapper extensions work fine with a Dictionary{string, object}, + // but some methods like QueryMultiple (when used with Stored Procedures) may require DynamicParameters + // TODO: should we just use DynamicParameters in all Dapper calls? + get + { + if (_dynamicParameters == null) + { + _dynamicParameters = new DynamicParameters(); + foreach (var parameter in this.Values) + SqlParameterMapper.Default.AddToDynamicParameters(_dynamicParameters, parameter); + } + return _dynamicParameters; + } + } + + /// + /// Add a explicit parameter to this dictionary + /// + public void Add(SqlParameterInfo parameter) + { + this[parameter.Name!] = parameter; + } + + + /// + /// Get parameter value + /// + public T Get(string key) => (T)this[key].Value; + + /// + /// Parameter Names + /// + public HashSet ParameterNames => new HashSet(this.Keys); + + void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + // IDynamicParameters is explicitly implemented (not public) - and it will add our dynamic paramaters to IDbCommand + ((SqlMapper.IDynamicParameters)DynamicParameters).AddParameters(command, identity); + } + + /// + /// After Dapper command is executed, we should get output/return parameters back + /// + void SqlMapper.IParameterCallbacks.OnCompleted() + { + var dapperParameters = DynamicParameters; + + // Update output and return parameters back + foreach (var oparm in this.Values.Where(p => p.ParameterDirection != ParameterDirection.Input && p.ParameterDirection != null)) + { + oparm.Value = dapperParameters.Get(oparm.Name); + oparm.OutputCallback?.Invoke(oparm.Value); + } + } + #endregion + + /// + /// Responsible for parsing SqlParameters (see ) + /// into a list of SqlParameterInfo that + /// + public static SqlParameterMapper InterpolatedSqlParameterParser = new SqlParameterMapper(); + } + +} diff --git a/src/DapperQueryBuilder/QueryBuilder.cs b/src/DapperQueryBuilder/QueryBuilder.cs index 9dd0a4b..8fa2212 100644 --- a/src/DapperQueryBuilder/QueryBuilder.cs +++ b/src/DapperQueryBuilder/QueryBuilder.cs @@ -1,30 +1,35 @@ -using Dapper; +using InterpolatedSql; using System; using System.Collections.Generic; using System.Data; -using System.Diagnostics; using System.Linq; using System.Text; -using System.Text.RegularExpressions; namespace DapperQueryBuilder { /// - /// Query Builder wraps an underlying SQL statement and the associated parameters.
- /// Allows to easily add new clauses to underlying statement and also add new parameters.
- /// On top of that it also loads a "Filters" property which can track a list of filters
- /// which are later combined (by default with AND) and will replace the keyword /**where**/ + /// Exactly like but also wraps an underlying IDbConnection + /// and has a "Filters" property which can track a list of filters which are later combined (by default with AND) and will replace the keyword /**where**/ ///
- public class QueryBuilder : ICompleteCommand + public class QueryBuilder : InterpolatedSqlBuilder, ICompleteCommand { #region Members - private readonly Filters _filters = new Filters(); - private readonly List _froms = new List(); - private readonly List _selects = new List(); - private readonly CommandBuilder _commandBuilder; - #endregion + private ParametersDictionary? _cachedDapperParameters = null; + + /// Sql Parameters converted into Dapper format + public ParametersDictionary DapperParameters => _cachedDapperParameters ?? (_cachedDapperParameters = ParametersDictionary.LoadFrom(this)); + + [Obsolete("Use DapperParameters")] public ParametersDictionary Parameters => DapperParameters; + + protected readonly Filters _filters = new Filters(); + protected readonly InterpolatedSqlBuilder _froms = new InterpolatedSqlBuilder(); + protected readonly InterpolatedSqlBuilder _selects = new InterpolatedSqlBuilder(); + protected InterpolatedSqlBuilder? _cachedCombinedQuery = null; + + // HACK: QueryBuilder inherits from InterpolatedStringBuilder to offer the Fluent API, but its final command should combine multiple blocks + // since base class methods use Format, in some moments (including in the constructor) we should NOT return the combined command + private bool _shouldBuildCombinedQuery = false; - #region Properties /// /// How a list of Filters are combined (AND operator or OR operator) /// @@ -42,9 +47,11 @@ public Filters.FiltersType FiltersType /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. /// Where filters will later replace /**where**/ keyword /// - public QueryBuilder(IDbConnection cnn) + public QueryBuilder(IDbConnection connection) : base() { - _commandBuilder = new CommandBuilder(cnn); + DbConnection = connection; + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + _shouldBuildCombinedQuery = true; } /// @@ -53,13 +60,15 @@ public QueryBuilder(IDbConnection cnn) /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. /// Where filters will later replace /**where**/ keyword /// - /// + /// /// You can use "{where}" or "/**where**/" in your query, and it will be replaced by "WHERE + filters" (if any filter is defined).
/// You can use "{filters}" or "/**filters**/" in your query, and it will be replaced by "AND filters" (without where) (if any filter is defined). /// - public QueryBuilder(IDbConnection cnn, FormattableString query) + public QueryBuilder(IDbConnection connection, FormattableString query) : base(query) { - _commandBuilder = new CommandBuilder(cnn, query); + DbConnection = connection; + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + _shouldBuildCombinedQuery = true; } #endregion @@ -69,8 +78,9 @@ public QueryBuilder(IDbConnection cnn, FormattableString query) /// public virtual QueryBuilder Where(Filter filter) { - filter.MergeParameters(_commandBuilder.Parameters); _filters.Add(filter); + PurgeLiteralCache(); + PurgeParametersCache(); return this; } @@ -79,11 +89,12 @@ public virtual QueryBuilder Where(Filter filter) /// public virtual QueryBuilder Where(Filters filters) { - filters.MergeParameters(_commandBuilder.Parameters); _filters.Add(filters); + PurgeLiteralCache(); + PurgeParametersCache(); return this; } - + /// /// Adds a new condition to where clauses.
@@ -99,158 +110,204 @@ public virtual QueryBuilder Where(FormattableString filter) /// Does NOT add leading "WHERE" keyword.
/// Returns null if no filter was defined. ///
- public string GetFilters() + public InterpolatedSqlBuilder? GetFilters() { if (_filters == null || !_filters.Any()) return null; - StringBuilder filtersString = new StringBuilder(); - _filters.WriteFilter(filtersString); // this writes all filters, going recursively if there are nested filters - return filtersString.ToString(); + InterpolatedSqlBuilder filters = new InterpolatedSqlBuilder(); + _filters.WriteTo(filters); // this writes all filters, going recursively if there are nested filters + return filters; } #endregion #region ICompleteCommand #region Sql + /// - /// + /// Gets the combined command /// - public string Sql + public virtual InterpolatedSqlBuilder CombinedQuery { get { - StringBuilder finalSql = new StringBuilder(); + if (_cachedCombinedQuery != null) + return _cachedCombinedQuery; - // If Query Template is provided, we assume it contains both SELECT and FROMs - if (_commandBuilder.Sql != null) - finalSql.Append(_commandBuilder.Sql); + InterpolatedSqlBuilder combinedQuery; - string filters = GetFilters(); - if (filters != null) + if (_format.Length > 0) { - if (finalSql.Length > 0 && finalSql.ToString().Contains("/**where**/")) - finalSql.Replace("/**where**/", "WHERE " + filters); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("{where}")) - finalSql.Replace("{where}", "WHERE " + filters); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("/**filters**/")) - finalSql.Replace("/**filters**/", "AND " + filters); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("{filters}")) - finalSql.Replace("{filters}", "AND " + filters); + _shouldBuildCombinedQuery = false; + combinedQuery = new InterpolatedSqlBuilder(Options, new StringBuilder(_format.Length).Append(_format), _sqlParameters.ToList()); + _shouldBuildCombinedQuery = true; + } + else + combinedQuery = new InterpolatedSqlBuilder(Options); + + if (_filters.Any()) + { + var filters = GetFilters()!; + + string matchKeyword; + int matchPos; + if (((matchKeyword = "/**where**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{where}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a Placeholder for Filters + filters.InsertLiteral(0, "WHERE "); + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, filters); + } + else if (((matchKeyword = "/**filters**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{filters}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a Placeholder for Filters + filters.InsertLiteral(0, "AND "); + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, filters); + } else { //TODO: if Query Template was provided, check if Template ends with "WHERE" or "WHERE 1=1" or "WHERE 0=0", or "WHERE 1=1 AND", etc. remove all that and replace. // else... //TODO: if Query Template was provided, check if Template ends has WHERE with real conditions... set hasWhereConditions=true // else... - finalSql.AppendLine("WHERE " + filters); + combinedQuery.Append(filters); } } - if (_froms.Any()) + if (_froms.Sql?.Length > 0) { - string froms = string.Join(Environment.NewLine, _froms); //TODO: bool AutoLineBreaks - if false don't join with NewLine - - if (finalSql.Length > 0 && finalSql.ToString().Contains("/**from**/")) - finalSql.Replace("/**from**/", "FROM " + froms); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("{from}")) - finalSql.Replace("{from}", "FROM " + froms); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("/**joins**/")) - finalSql.Replace("/**joins**/", froms); - else if (finalSql.Length > 0 && finalSql.ToString().Contains("{joins}")) - finalSql.Replace("{joins}", froms); + _froms.TrimEnd(); + string matchKeyword; + int matchPos; + if (((matchKeyword = "/**from**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{from}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a Placeholder for FROMs + _froms.InsertLiteral(0, "FROM "); + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, _froms); + } + else if (((matchKeyword = "/**joins**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{joins}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a placeholder for JOINS (yeah - JOINS and FROMS are currently using same variable) + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, _froms); + } } - if (_selects.Any()) + if (_selects.Sql?.Length > 0) { - string selects = string.Join(", ", _selects); + _selects.TrimEnd(); - if (finalSql.Length > 0) + if (_selects.Sql?.Length > 0) { - if ( finalSql.ToString().Contains("/**select**/")) - finalSql.Replace("/**select**/", "SELECT " + selects); - else if ( finalSql.ToString().Contains("{select}")) - finalSql.Replace("{select}", "SELECT " + selects); - else if (finalSql.ToString().Contains("/**selects**/")) - finalSql.Replace("/**selects**/", ", " + selects); - else if ( finalSql.ToString().Contains("{selects}")) - finalSql.Replace("{selects}", ", " + selects); + string matchKeyword; + int matchPos; + if (((matchKeyword = "/**select**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{select}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a Placeholder for SELECT + _selects.InsertLiteral(0, "SELECT "); + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, _selects); + } + else if (((matchKeyword = "/**selects**/") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1) || + ((matchKeyword = "{selects}") != null && (matchPos = combinedQuery.IndexOf(matchKeyword)) != -1)) + { + // Template has a placeholder for SELECTS - which means that + // SELECT should be already in template and user just wants to add more columns using "selects" placeholder + _selects.InsertLiteral(0, ", "); + combinedQuery.Remove(matchPos, matchKeyword.Length); + combinedQuery.Insert(matchPos, _selects); + } } } - return finalSql.ToString(); + _cachedCombinedQuery = combinedQuery; + return _cachedCombinedQuery; } } #endregion - /// - /// Parameters of Query - /// - public ParameterInfos Parameters => _commandBuilder.Parameters; + /// + public override IReadOnlyList SqlParameters => _shouldBuildCombinedQuery ? CombinedQuery.SqlParameters : base.SqlParameters; - /// - /// Underlying connection - /// - public IDbConnection Connection => _commandBuilder.Connection; - #endregion + /// + public override string Format => _shouldBuildCombinedQuery ? CombinedQuery.Format : base.Format; - /// - /// Appends a statement to the current query.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. - ///
- /// SQL command - public QueryBuilder Append(FormattableString statement) + /// + protected override void PurgeLiteralCache() { - _commandBuilder.Append(statement); - return this; + _cachedCombinedQuery = null; + base.PurgeLiteralCache(); } + #endregion + +#if NET6_0_OR_GREATER /// - /// Appends a statement to the current query, but before statement adds a linebreak.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. + /// Adds a new join to the FROM clause. ///
- /// SQL command - public QueryBuilder AppendLine(FormattableString statement) + public virtual QueryBuilder From(ref InterpolatedSqlHandler value) { - _commandBuilder.AppendLine(statement); + _froms.Append(value.InterpolatedSqlBuilder); + _froms.AppendLiteral(NewLine); //TODO: bool AutoLineBreaks + PurgeLiteralCache(); + PurgeParametersCache(); return this; } +#endif /// - /// Appends a statement to the current query.
- /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. + /// Adds a new join to the FROM clause. ///
- public static QueryBuilder operator +(QueryBuilder cmd, FormattableString fs) + public virtual QueryBuilder From(LegacyFormattableString fromString) { - return cmd.Append(fs); + _froms.Append(fromString); + _froms.AppendLiteral(NewLine); //TODO: bool AutoLineBreaks + PurgeLiteralCache(); + PurgeParametersCache(); + return this; } +#if NET6_0_OR_GREATER /// - /// Adds a new join to the FROM clause. + /// Adds a new column to the SELECT clause. /// - public virtual QueryBuilder From(FormattableString fromString) + public virtual QueryBuilder Select(ref InterpolatedSqlHandler value) { - var parsedStatement = new InterpolatedStatementParser(fromString); - string sql = parsedStatement.Sql; - if (parsedStatement.Parameters.Any()) - { - sql = (Parameters.MergeParameters(parsedStatement.Parameters, parsedStatement.Sql) ?? sql); - } - _froms.Add(sql); + if (!_selects.IsEmpty) + _selects.AppendLiteral(", "); + if (value.InterpolatedSqlBuilder.SqlParameters.Count == 0) // if it's just a plain string, then it's code (not user input) - so it's safe // TODO: review this! should we always get strings here? what about SELECT [expression] ? allow both? + _selects.AppendLiteral(value.InterpolatedSqlBuilder.Format); + else + _selects.Append(value.InterpolatedSqlBuilder); + PurgeLiteralCache(); + PurgeParametersCache(); return this; } +#endif /// /// Adds a new column to the SELECT clause. /// - public virtual QueryBuilder Select(FormattableString selectString) + public virtual QueryBuilder Select(LegacyFormattableString selectString) { - var parsedStatement = new InterpolatedStatementParser(selectString); - if (parsedStatement.Parameters.Any()) - _selects.Add(Parameters.MergeParameters(parsedStatement.Parameters, parsedStatement.Sql)); + if (!_selects.IsEmpty) + _selects.AppendLiteral(", "); + if (((FormattableString)selectString).ArgumentCount == 0) // if it's just a plain string, then it's code (not user input) - so it's safe // TODO: review this! should we always get strings here? what about SELECT [expression] ? allow both? + _selects.AppendLiteral(((FormattableString)selectString).Format); else - _selects.Add(parsedStatement.Sql); + _selects.Append(selectString); + PurgeLiteralCache(); + PurgeParametersCache(); return this; } + } } diff --git a/src/DapperQueryBuilder/SqlBuilder.cs b/src/DapperQueryBuilder/SqlBuilder.cs new file mode 100644 index 0000000..d347f77 --- /dev/null +++ b/src/DapperQueryBuilder/SqlBuilder.cs @@ -0,0 +1,75 @@ +using InterpolatedSql; +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace DapperQueryBuilder +{ + /// + /// Exactly like but it requires an underlying IDbConnection, + /// provides facades (as extension-methods) to invoke Dapper extensions (see ), + /// and maps to Dapper type. + /// + public class SqlBuilder : InterpolatedSqlBuilder, ICompleteCommand + { + #region Members + private ParametersDictionary? _cachedDapperParameters = null; + + /// Sql Parameters converted into Dapper format + public ParametersDictionary DapperParameters => _cachedDapperParameters ?? (_cachedDapperParameters = ParametersDictionary.LoadFrom(this)); + + [Obsolete("Use DapperParameters")] public ParametersDictionary Parameters => DapperParameters; + #endregion + + #region ctors + /// + protected internal SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options, StringBuilder? format, List? arguments) : base(options, format, arguments) + { + DbConnection = connection; + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + } + + /// + public SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options = null) : base(options) + { + DbConnection = connection; + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + } + + /// + /// New CommandBuilder based on an initial command.
+ /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. + ///
+ /// Underlying connection + /// SQL command + public SqlBuilder(IDbConnection cnn, FormattableString command, InterpolatedSqlBuilderOptions? options = null) : this(cnn, options) + { + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + AppendFormattableString(command); + } + + /// + /// New CommandBuilder based on an initial command.
+ /// Parameters embedded using string-interpolation will be automatically converted into Dapper parameters. + ///
+ /// Underlying connection + /// SQL command + public SqlBuilder(IDbConnection cnn, InterpolatedSqlBuilder command, InterpolatedSqlBuilderOptions? options = null) : this(cnn, options) + { + Options.CalculateAutoParameterName = (parameter, pos) => DapperQueryBuilderOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); + Append(command); + } + #endregion + + /// + protected override void PurgeParametersCache() + { + base.PurgeParametersCache(); + _cachedDapperParameters = null; + } + } +} diff --git a/src/DapperQueryBuilder/SqlParameterMapper.cs b/src/DapperQueryBuilder/SqlParameterMapper.cs new file mode 100644 index 0000000..2611b81 --- /dev/null +++ b/src/DapperQueryBuilder/SqlParameterMapper.cs @@ -0,0 +1,58 @@ +using Dapper; +using InterpolatedSql; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace DapperQueryBuilder +{ + /// + /// Maps from to Dapper Parameters. + /// + public class SqlParameterMapper + { + /// + /// Calculates the name automatically assigned to interpolated parameters + /// + public virtual string CalculateAutoParameterName(InterpolatedSqlParameter parameter, int pos, InterpolatedSqlBuilderOptions options) + { + return options.AutoGeneratedParameterPrefix + + (IsEnumerable(parameter.Argument) ? options.ParameterArrayNameSuffix : "") + + pos.ToString(); + } + + private bool IsEnumerable(object? value) + { + if (value == null || value is DBNull) //SqlMapper.GetDbType + return false; + Type t = value.GetType(); + return t != typeof(string) && typeof(IEnumerable).IsAssignableFrom(t); + //TODO: use Dapper SqlMapper.LookupDbType ? + } + + /// + /// Converts from to Dapper Parameters. + /// + public virtual void AddToDynamicParameters(DynamicParameters target, SqlParameterInfo parameter) + { + //TODO: do implicit parameters have names here?! + if (parameter is DbTypeParameterInfo dbParm) + target.Add(parameter.Name, parameter.Value, dbParm.DbType, parameter.ParameterDirection ?? ParameterDirection.Input, dbParm.Size); + else if (parameter is StringParameterInfo stringParm) + target.Add(parameter.Name, new DbString() { Value = (string?)stringParm.Value, IsAnsi = stringParm.IsAnsi, IsFixedLength = stringParm.IsFixedLength, Length = stringParm.Length }); + else if (parameter is SqlParameterInfo parm && parm.Value is IEnumerable stringParms) + { + target.Add(parameter.Name, stringParms.Select(stringParm => new DbString() { Value = (string?)stringParm.Value, IsAnsi = stringParm.IsAnsi, IsFixedLength = stringParm.IsFixedLength, Length = stringParm.Length })); + } + else + target.Add(parameter.Name, parameter.Value); + } + + /// + /// Default mapper. By inheriting/overriding it's possible to modify this behavior + /// + public static SqlParameterMapper Default = new SqlParameterMapper(); + } +} diff --git a/src/build.ps1 b/src/build.ps1 index 620434e..33df42e 100644 --- a/src/build.ps1 +++ b/src/build.ps1 @@ -26,7 +26,7 @@ dotnet build -c release DapperQueryBuilder\DapperQueryBuilder.csproj & $msbuild "DapperQueryBuilder\DapperQueryBuilder.csproj" ` /t:Pack ` /p:PackageOutputPath="..\packages-local\" ` - '/p:targetFrameworks="net461;netstandard2.0;net5.0"' ` + '/p:targetFrameworks="netstandard2.0;net462;net472;net5.0;net6.0;net7.0"' ` /p:Configuration=$configuration ` /p:IncludeSymbols=true ` /p:SymbolPackageFormat=snupkg ` @@ -38,7 +38,7 @@ dotnet build -c release DapperQueryBuilder.StrongName\DapperQueryBuilder.StrongN & $msbuild "DapperQueryBuilder.StrongName\DapperQueryBuilder.StrongName.csproj" ` /t:Pack ` /p:PackageOutputPath="..\packages-local\" ` - '/p:targetFrameworks="net461;netstandard2.0;net5.0"' ` + '/p:targetFrameworks="netstandard2.0;net462;net472;net5.0;net6.0;net7.0"' ` /p:Configuration=$configuration ` /p:IncludeSymbols=true ` /p:SymbolPackageFormat=snupkg `